提交金刚顺丰前端H5

This commit is contained in:
有果 2026-03-16 11:10:28 +08:00
parent 04f40fbf41
commit 9c1d98f36f
1066 changed files with 141326 additions and 2 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/unpackage
/node_modules

16
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,16 @@
{ // launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

40
Apis/book.js Normal file
View File

@ -0,0 +1,40 @@
import request from "/utils/request.js";
export function ClientSite() {
return {
// 查看所有门店
getSiteDetailsAll: (data) => {
return request.request({
url: `/ClientSite/GetSiteDetailsAll`,
data,
method: 'get',
});
},
// 根据门店ID查路线图
GetSiteGuideById: (id) => {
return request.request({
url:`/ClientSite/GetSiteGuideById?siteId=${id}`,
method: 'get',
});
},
// 获取门店所在的所有城市
GetCityAll: () => {
return request.request({
url: '/ClientSite/GetCityAll',
method: 'get',
});
},
// 根据门店所在的城市获取对应的区域
GetDistrictByCity: (city) => {
return request.request({
url: `/ClientSite/GetDistrictByCity?city=${city}`,
method: 'get',
});
},
GetSiteStopCarGuideById: (id) => {
return request.request({
url:`/ClientSite/GetSiteStopCarGuideById?siteId=${id}`,
method: 'get',
});
},
}
}

55
Apis/clientCustomer.js Normal file
View File

@ -0,0 +1,55 @@
import request from '/utils/request.js';
export function getClientCustomerApi() {
return {
// 获取中介二维码
GetMediatorQrCode: () => {
return request.request({
url: '/ClientMediator/GetMediatorQrCode',
method: 'get'
});
},
// 获取中介信息
GetMediatorInfoById: () => {
return request.request({
url: '/ClientMediator/GetMediatorInfoById',
method: 'get'
});
},
// 修改中介信息
UpdateMediatorInfo: (data) => {
return request.request({
url: '/ClientMediator/UpdateMediatorInfo',
method: 'post',
data
});
},
// 获取中介的邀请记录
GetMediatorUpUserList: () => {
return request.request({
url: '/ClientMediator/GetMediatorUpUserList',
method: 'post'
});
},
// 获取当前用户绑定的中介信息
GetMediatorByUser: () => {
return request.request({
url: '/ClientMediator/GetMediatorByUser',
method: 'get'
});
},
GetMediatorCompanyAllList: () => {
return request.request({
url: '/ClientMediator/GetMediatorCompanyAllList',
method: 'get'
});
},
ApplyMediator: (data) => {
return request.request({
url: '/ClientMediator/ApplyMediator',
method: 'post',
data
});
}
};
}

37
Apis/coupon.js Normal file
View File

@ -0,0 +1,37 @@
import request from "/utils/request.js";
export function couponApi() {
return {
// 获取优惠卷列表
GetCouponList: (data) => {
return request.request({
url: '/ClientCoupon/GetCouponList',
method: 'get',
data,
});
},
// 领取优惠卷
DrawDownCoupon: (data) => {
return request.request({
url: '/ClientCoupon/DrawDownCoupon?couponCode='+data.couponCode,
method: 'post',
data,
});
},
// 优惠卷弹窗
GetNewUserCouponCode: (data) => {
return request.request({
url: '/ClientCoupon/GetNewUserCouponCode',
method: 'get',
data,
});
},
// 美团优惠卷
GetMeiTuanCodeByPhone: (data) => {
return request.request({
url: '/MeiTuan/GetMeiTuanCodeByPhone',
method: 'get',
data,
});
}
};
}

28
Apis/goodsList.js Normal file
View File

@ -0,0 +1,28 @@
import request from "/utils/request.js";
export function useGoodsApi() {
return {
// 物品清单
GetGoodsList: (data) => {
return request.request({
url: `/ClientOrder/GetGoodsList`,
data,
method: 'get',
});
},
// 获取设置过的物品清单
GetSubmitGoodsList: (id) => {
return request.request({
url:`/ClientOrder/GetSubmitGoodsList?orderId=${id}`,
method: 'get',
});
},
// 提交物品清单
SubmitGoodsList: (data) => {
return request.request({
url: '/ClientOrder/SubmitGoodsList',
method: 'post',
data
});
},
}
}

63
Apis/home.js Normal file
View File

@ -0,0 +1,63 @@
import request from "/utils/request.js";
export function useLoginApi() {
return {
//获取code img
getCode: () => {
return request.request({
url: '/Login/Captcha',
method: 'get',
});
},
//登录
signIn: (data) => {
return request.request({
url: '/Login/Login',
method: 'post',
data,
});
},
signOut: (data) => {
return request.request({
url: '/user/signOut',
method: 'post',
data,
});
},
GetUnitTypeAll: () => {
return request.request({
url: '/ClientSite/GetUnitTypeAll',
method: 'get',
})
},
GetHeatSites: () => {
return request.request({
url: '/ClientSite/GetHeatSites',
method: 'get',
})
},
// 理解預約
CreateReservation:(data)=>{
return request.request({
url: '/ClientReservation/CreateReservation',
method: 'post',
data
})
},
// 获取小程序内容
GetPageContent:(data)=>{
return request.request({
url: '/ClientPageContent/GetPageContent',
method: 'get',
data
})
},
// 获取限时抢购入口
GetFlashSaleEntrance:(data) => {
return request.request({
url: '/ClientSite/GetFlashSaleEntrance',
method: 'get',
data,
});
}
};
}

55
Apis/invoice.js Normal file
View File

@ -0,0 +1,55 @@
import request from "/utils/request.js";
export function useInvoiceApi() {
return {
// 发票申请
InvoiceApplyFor: (data) => {
return request.request({
url: '/ClientInvoice/InvoiceApplyFor',
method: 'post',
data,
});
},
// 获取可开票订单列表
GetCanInvoiceList: (data) => {
return request.request({
url: '/ClientOrder/GetCanInvoiceList',
method: 'get',
data,
}
)
},
// 获取申请开票列表
GetInvoiceApplyFor: (data) => {
return request.request({
url: '/ClientInvoice/GetInvoiceApplyFor',
method: 'get',
data,
}
)
},
// 取消开票
CancelInvoiceApplyFor: (data) => {
return request.request({
url: '/ClientInvoice/CancelInvoiceApplyFor',
method: 'get',
data,
})
},
// 获取申请开票详情
GetInvoiceApplyForById: (data) => {
return request.request({
url: '/ClientInvoice/GetInvoiceApplyForById',
method: 'get',
data,
})
},
// 修改发票
UpdateInvoiceApplyFor: (data) => {
return request.request({
url: '/ClientInvoice/UpdateInvoiceApplyFor',
method: 'post',
data,
})
}
}
}

139
Apis/lock.js Normal file
View File

@ -0,0 +1,139 @@
import request from "/utils/request.js";
export function useLockApi() {
return {
GetDyncPwdByMac: (data) => {
return request.request({
url: '/LockOperation/GetDyncPwd',
method: 'get',
data,
});
},
GetAccesscontrolQRCodeBySite: (data) => {
return request.request({
url: '/Accesscontrol/GetAccesscontrolQRCode',
method: 'get',
data,
});
},
// 门禁远程开门
RemoteOpenDoor: (data) => {
return request.request({
url: '/Accesscontrol/RemoteOpenDoor',
method: 'get',
data,
});
},
//通通锁远程开锁
RemoteOpen: (data) => {
return request.request({
url: '/LockOperation/RemoteOpen',
method: 'post',
data,
});
},
// 获取初始化的通通锁列表
GetInitLockList: (data) => {
return request.request({
url: '/Lock/GetLockInfoByOpenId',
method: 'get',
data,
});
},
// 获取初始化的通通锁信息
SaveInitLock: (data) => {
return request.request({
url: '/Lock/PushLockInfo',
method: 'post',
data,
});
},
//授权订单
OrderAuthorizeCustomer: (data) => {
return request.request({
url: '/ClientOrder/OrderAuthorizeCustomer',
method: 'post',
data,
});
},
// 更新授权订单
UpdateOrderAuthorizeCustomer: (data) => {
return request.request({
url: '/ClientOrder/UpdateOrderAuthorizeCustomer',
method: 'post',
data,
});
},
// 删除授权订单
DeleteOrderAuthorize: (data) => {
return request.request({
url: '/ClientOrder/DeleteOrderAuthorize?authorizeId=' + data.authorizeId,
method: 'delete',
data,
});
},
// 获取授权列表
GetOrderAuthorizeList: (data) => {
return request.request({
url: '/ClientOrder/GetOrderAuthorizeList',
method: 'get',
data,
});
},
// 获取授权的订单
GetAuthorizeOrderList: (data) => {
return request.request({
url: '/ClientOrder/GetAuthorizeOrderList',
method: 'post',
data,
});
},
// 设置用户固定密码
SetUserFixedPassword: (data) => {
return request.request({
url: '/LockOperation/SetUserFixedPassword',
method: 'post',
data,
});
},
// 门禁绑卡
AddCardNumber: (data) => {
return request.request({
url: '/Site/AddCardNumber',
method: 'get',
data,
});
},
//通通锁绑卡
BindCardByWifiSmartLock: (data) => {
return request.request({
url: '/LockOperation/BindCardByWifiSmartLock',
method: 'post',
data,
});
},
// zoned 锁绑卡
SetRFIDCard: (data) => {
return request.request({
url: '/LockOperation/SetRFIDCard',
method: 'post',
data,
});
},
// zoned 绑卡读取卡结果
GetBindingCardResult: (data) => {
return request.request({
url: '/LockOperation/GetBindingCardResult',
method: 'get',
data,
});
},
// 就门禁id 获取门禁信息
GetNewLockerId: (data) => {
return request.request({
url: '/Locker/GetNewLockerId',
method: 'get',
data,
});
},
};
}

159
Apis/login.js Normal file
View File

@ -0,0 +1,159 @@
import request from "/utils/request.js";
export function useLoginApi() {
return {
//获取code img
getCode: () => {
return request.request({
url: '/Login/Captcha',
method: 'get',
});
},
//登录
Login: (data) => {
return request.request({
url: '/ClientCustomer/Login',
method: 'post',
data,
});
},
Register: (data) => {
return request.request({
url: '/ClientCustomer/Register',
method: 'post',
data,
});
},
EmailVerify: (data) => {
return request.request({
url: '/ClientCustomer/EmailVerify',
method: 'post',
data,
});
},
ForgotPassword: (data) => {
return request.request({
url: '/ClientCustomer/ForgotPassword',
method: 'post',
data,
headers: {
"Content-Type": "application/json; charset=utf-8",
}
});
},
UpdateUserInfo: (data) => {
return request.request({
url: '/ClientCustomer/UpdateUserInfo',
method: 'post',
data,
});
},
// 微信登录
AuthorizedLogin: (data) => {
return request.request({
url:'/ClientCustomer/AuthorizedLogin',
method:'post',
data,
})
},
// 通过用户授权的code去获取手机号码
GetPhoneNumber: (data) => {
return request.request({
url:`/ClientCustomer/GetPhoneNumber`,
method:'get',
params:{
...data,
isUpdate:true
},
})
},
// 通过用户授权的code去获取手机号码
GetPhoneNumberNoUpdate: (data) => {
return request.request({
url:`/ClientCustomer/GetPhoneNumber?code=${data}`,
method:'get',
data,
})
},
// 获取用户信息
GetUserInfo:() =>{
return request.request({
url:`/ClientCustomer/GetUserInfo`,
method:'get',
})
},
// 更新用户信息
EditUserInfo: (data) => {
return request.request({
url: '/ClientCustomer/EditUserInfo',
method: 'post',
data,
});
},
// 从主题二维码来的
GetActivitiesCode: (data) => {
return request.request({
url: '/ClientCustomer/GetActivitiesCode',
method: 'post',
data,
});
},
// 获取openId
GetOpenId: (data) => {
return request.request({
url:'/ClientCustomer/GetOpenId',
method:'post',
data,
})
},
// 获取积分豆
GetCustomerPoint: () => {
return request.request({
url: '/ClientCustomer/GetCustomerPoint',
method: 'get',
});
},
// 兑换奖品
CustomerExchangeGift: (data) => {
return request.request({
url: '/ClientPoint/CustomerExchangeGift',
method: 'post',
data,
});
},
GetGiftInfo: (data) => {
return request.request({
url: '/ClientPoint/GetGiftInfo',
method: 'post',
data,
});
},
GetGiftList: () => {
return request.request({
url: '/ClientPoint/GetGiftList',
method: 'get',
});
},
GetCustomerExchangeGift: (data) => {
return request.request({
url: '/ClientPoint/GetCustomerExchangeGift',
method: 'get',
data,
});
},
GetCustomerList: (data) => {
return request.request({
url: '/Customer/GetCustomerList',
method: 'get',
data,
});
},
ShunFengLogin: (data) => {
return request.request({
url: '/ShunFeng/ShunFengLogin',
method: 'post',
data,
});
}
};
}

172
Apis/order.js Normal file
View File

@ -0,0 +1,172 @@
import request from "/utils/request.js";
export function useOrderApi() {
return {
GetOrderById: (data) => {
return request.request({
url: '/ClientOrder/GetOrderById',
method: 'get',
data,
});
},
GetOrderList: (data) => {
return request.request({
url: '/ClientOrder/GetOrderList',
method: 'get',
data,
});
},
AddOrder: (data) => {
return request.request({
url: '/ClientOrder/AddOrder',
method: 'post',
data,
});
},
AddOrder2: (data) => {
return request.request({
url: '/ClientOrder/AddOrder2',
method: 'post',
data,
});
},
UploaderImage: (data) => {
return request.uploadFile({
url: '/ClientImages/UploadFileByALiYun',
method: 'post',
data,
headers:{'Content-Type':'multipart/form-data'}
});
},
ApplyForRefundLocker: (data) => {
return request.request({
url: '/ClientOrder/ApplyForRefundLocker',
method: 'post',
data,
});
},
CancelApplyForRefundLocker: (data) => {
return request.request({
url: `/ClientOrder/CancelApplyForRefundLocker?orderId=${data}`,
method: 'post',
data,
});
},
SubmitOrderEvaluate: (data) => {
return request.request({
url: '/ClientOrder/SubmitOrderEvaluate',
method: 'post',
data,
});
},
//续租订单价格
ContinuationOrderPrice:(data)=>{
return request.request({
url: '/ClientOrder/ContinuationOrderPrice',
method: 'get',
data,
})
},
//续租订单价格
ContinuationOrderPricePost:(data)=>{
return request.request({
url: '/ClientOrder/ContinuationOrderPrice',
method: 'post',
data,
})
},
//续租订单
ContinuationOrder:(data)=>{
return request.request({
url: '/ClientOrder/ContinuationOrder',
method: 'post',
data,
})
},
//续租订单
ContinuationOrderH5:(data)=>{
return request.request({
url: '/ClientOrder/ContinuationOrderH5',
method: 'post',
data,
})
},
// 关闭支付
CloseWeChatPayment:(data)=>{
return request.request({
url: `/ClientOrder/CloseWeChatPayment?out_trade_no=${data.out_trade_no}`,
method: 'post',
data,
})
},
//授权
OrderAuthorization:(data)=>{
return request.request({
url: '/ClientOrder/OrderAuthorization',
method: 'post',
data,
})
},
// 获取信息
GetOrderAuthorizationFace:(data)=>{
return request.request({
url: '/ClientOrder/GetOrderAuthorizationFace',
method: 'get',
data,
})
},
// 申请退押金
WeChatMerchantRefund:(data)=>{
return request.request({
url: '/ClientOrder/WeChatMerchantRefund',
method: 'get',
data,
})
},
// 获取起租天数
GetStartDateRntalByKey: () => {
return request.request({
url: '/sysconfig/GetStartDateRntalByKey',
method: 'get'
});
},
GenerateQuotation : (data) => {
return request.request({
url: '/ClientSite/GenerateQuotation',
method: 'post',
data,
responseType: 'arraybuffer'
});
},
// 获取锁订单时间
GetLockOrderTime: (data) => {
return request.request({
url: '/ClientOrder/GetLockOrderTime',
method: 'post',
params: data,
});
},
// 取消支付
OrderCountdownTime: (data) => {
return request.request({
url: '/ClientOrder/OrderCountdownTime',
method: 'post',
params: data,
});
},
// 继续支付
ContinueOrderPay: (data) => {
return request.request({
url: '/ClientOrder/ContinueOrderPay',
method: 'post',
params: data,
});
},
GetAppText: (data) => {
return request.request({
url: '/APP/GetAppText',
method: 'get',
data
});
}
}
}

21
Apis/recommend.js Normal file
View File

@ -0,0 +1,21 @@
import request from "/utils/request.js";
export function useRecommend() {
return {
// 获取推荐列表
GetRecommend: (data) => {
return request.request({
url: '/ClientCustomer/GetRecommend',
data,
method: 'get',
});
},
// 获取推荐人数
GetRecommendCount: (data) => {
return request.request({
url: '/ClientCustomer/GetRecommendCount',
data,
method: 'get',
});
}
}
}

70
Apis/site.js Normal file
View File

@ -0,0 +1,70 @@
import request from "/utils/request.js";
export function useSiteApi() {
return {
// 根據門店id 獲取門店
GetUnitTypeBySiteId: (data) => {
return request.request({
url: '/ClientSite/GetUnitTypeBySiteId',
method: 'get',
data,
});
},
GetLockerBySiteId: (data) => {
return request.request({
url: '/ClientSite/GetLockerBySiteIdList',
method: 'get',
data,
});
},
GetLockerById: (data) => {
return request.request({
url: '/ClientSite/GetLockerById',
method: 'get',
data,
});
},
GetLockerExpense: (data) => {
return request.request({
url: '/ClientSite/GetLockerExpense',
method: 'post',
data,
});
},
AlternateReservation: (data) => {
return request.request({
url:'/ClientUnitType/AlternateReservation',
method: 'post',
data,
});
},
GetReserveIsEnable: (data) => {
return request.request({
url:'/ClientUnitType/GetReserveIsEnable',
method: 'get',
data,
});
},
// 取消預約
CancelReservation: (data) => {
return request.request({
url:'/ClientUnitType/CancelReservation',
method: 'post',
data,
});
},
// 获取五羊门店
GetMultipleStoreInfo: () => {
return request.request({
url: '/ClientSite/GetMultipleStoreInfo',
method: 'get',
});
},
GetLockerAgreementHTMLById: (data) => {
return request.request({
url: '/ClientSite/GetLockerAgreementHTMLById',
method: 'get',
data,
});
}
};
}

67
Apis/validInfo.js Normal file
View File

@ -0,0 +1,67 @@
import request from "/utils/request.js";
export function authInfoApi() {
return {
// 获取认证列表
GetCertificateList: (data) => {
return request.request({
url: '/InfoCertification/GetCertificateList',
method: 'get',
data,
});
},
// 获取是否认证过
GetIsCertification: () => {
return request.request({
url: '/ClientInfoCertification/GetIsCertification',
method: 'get',
});
},
// 获取认证详情
GetCertificationInfo: () => {
return request.request({
url: '/ClientInfoCertification/GetCertificateByUserId',
method: 'get',
});
},
// 提交企业认证
SubmitEnterpriseCertification: (data) => {
return request.request({
url: '/ClientInfoCertification/SubmitEnterpriseCertification',
method: 'post',
data,
});
},
// 提交个人认证
SubmitPersonCertification: (data) => {
return request.request({
url: '/ClientInfoCertification/SubmitIndividualCertification',
method: 'post',
data,
});
},
// 修改验证信息
UpdateCertification: (data) => {
return request.request({
url: '/ClientInfoCertification/UpdateCertification',
method: 'post',
data,
});
},
// 领取优惠卷
DrawDownCoupon: (data) => {
return request.request({
url: '/ClientCoupon/DrawDownCoupon?couponCode='+data.couponCode,
method: 'post',
data,
});
},
// 优惠卷弹窗
GetNewUserCouponCode: (data) => {
return request.request({
url: '/ClientCoupon/GetNewUserCouponCode',
method: 'get',
data,
});
}
};
}

241
App.vue Normal file
View File

@ -0,0 +1,241 @@
<script>
import { useMainStore } from "@/store/index.js";
import { shunfenLogin,getQueryParam } from "@/utils/common";
import { useLoginApi } from "@/Apis/login.js";
const getApi = useLoginApi();
const updateManagerFn = () => {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
//
console.log(res.hasUpdate, "请求完新版本信息的回调");
});
updateManager.onUpdateReady(function (res) {
uni.showModal({
title: "更新提示",
content: "新版本已经准备好,是否重启应用?",
showCancel: false,
success(res) {
if (res.confirm) {
// applyUpdate
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(function (res) {
//
uni.showModal({
title: "提示",
content: "新版小程序下载失败\n请自行退出程序手动卸载本程序再运行",
confirmText: "知道了",
});
});
};
const listenNetworkChange = () => {
uni.onNetworkStatusChange((res) => {
if (res.isConnected) {
uni.showToast({
title: "网络已连接",
icon: "none",
duration: 3000,
});
} else {
uni.showToast({
title: "当前无网络连接",
icon: "none",
duration: 3000,
});
}
});
};
export default {
globalData: {
statusBarHeight: 0,
navbarHeight: 0,
},
onLaunch: async function () {
uni.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
uni.showToast({
title: '当前无网络连接',
icon: 'none',
duration: 3000
});
}
}
});
//
listenNetworkChange();
uni.hideTabBar()
const { setTheme, getUserInfo, storeState,logOut } = useMainStore();
setTheme();
// #ifdef MP-WEIXIN || MP-XHS
//
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
const wxMenuBtn = uni.getMenuButtonBoundingClientRect();
// () = + ( - ) * 2
const barHeight = wxMenuBtn.height + (wxMenuBtn.top - statusBarHeight) * 2;
// = +
this.globalData.navbarHeight = (barHeight || 40) + statusBarHeight;
this.globalData.statusBarHeight = statusBarHeight || 20;
// #endif
// authCode
const authCode = getQueryParam('authCode');
if(authCode){
let source = window.sf.isSfApp() ? 2 : 1;
getApi.ShunFengLogin({
code:authCode,
source,
})
.then(async(res) => {
uni.hideLoading();
storeState.hasTrytoLogin = true;
if (res.code == 200) {
uni.setStorageSync("token", res.data.token);
//
await getUserInfo();
console.log('用户信息',storeState.userInfo)
if(storeState.userInfo.phone){
uni.$emit('loginSuccess',{msg:'页面更新'})
storeState.token = res.data.token;
uni.setStorageSync("token", res.data.token);
uni.setStorageSync("openId", res.data.openId);
}else{
uni.removeStorageSync("token");
console.log('用户未授权手机号,启动shunfenLogin')
shunfenLogin();
}
//
}else{
logOut();
}
}).catch((err) => {
logOut();
})
}
//
uni.removeStorageSync('getCouponCodeTime');
},
onShow: function () {
// #ifdef MP-WEIXIN
//
updateManagerFn();
// #endif
},
onHide: function () { },
onPageNotFound() {
uni.switchTab({
url: "/pages/index/index",
});
},
};
</script>
<style lang="scss">
/*每个页面公共css */
@import "@/uni_modules/uni-scss/index.scss";
@import "@/static/iconfont/iconfont.css";
// @import "@/static/style/theme.scss";
/* #ifndef APP-NVUE */
@import "@/static/customicons.css";
//
[hidden] {
display: none !important;
}
html{
height: unset;
min-height: 100vh;
}
body {
background:$backgroundColor;
min-height: 100vh;
}
page {
// uni-input,
// uni-button,
// uni-textarea {
// line-height: 2.55555;
// }
}
view,
text {
box-sizing: border-box;
}
.myCustomTabbar {
position: fixed;
bottom: -1px;
left: 0;
padding:20rpx 0;
padding-bottom: 40rpx;
background-color: #fff;
height: 160upx;
.uni-tabbar__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
}
.uni-tabbar {
height: 200upx;
}
.uni-tabbar__label {
color: #000;
font-size: 24rpx !important;
margin-top: 8px;
}
.uni-tabbar__item.call-phone-item {
position: relative;
.uni-tabbar__bd {
top: -80rpx;
width: 160rpx;
position: absolute;
height: 160rpx;
.uni-tabbar__icon {
width: 40rpx;
height: 40rpx;
svg {
max-width: 100%;
height: 40rpx;
width: 40rpx;
}
}
border-radius: 1000rpx;
background: linear-gradient(180deg,
var(--left-linear),
var(--right-linear) 100%);
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.2);
}
}
}
.uni-app--showleftwindow+.uni-tabbar-bottom {
display: none;
}
/* #endif */
.example-info {
font-size: 14px;
color: #333;
padding: 10px;
}
</style>

View File

@ -1,3 +1,20 @@
# SFH5
# Introduction
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
金刚顺丰H5
# Getting Started
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
1. Installation process
2. Software dependencies
3. Latest releases
4. API references
# Build and Test
TODO: Describe and show how to build your code and run the tests.
# Contribute
TODO: Explain how other users and developers can contribute to make your code better.
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
- [ASP.NET Core](https://github.com/aspnet/Home)
- [Visual Studio Code](https://github.com/Microsoft/vscode)
- [Chakra Core](https://github.com/Microsoft/ChakraCore)

View File

@ -0,0 +1,246 @@
<template>
<view class="agreement-wrapper">
<checkbox-group @change="onChange">
<label class="agreement-label">
<checkbox :checked="checked" style="transform:scale(0.8)" />
<text class="agreement-text">
{{ $t('agreement.readAndAgree') }}
<text class="link" @click.stop="open('service')">
{{ $t('agreement.service') }}
</text>
{{ $t('agreement.and') }}
<text class="link" @click.stop="open('privacy')">
{{ $t('agreement.privacy') }}
</text>
</text>
</label>
</checkbox-group>
<!-- 协议弹窗 -->
<uni-popup ref="popup" type="center">
<view class="popup-box">
<scroll-view scroll-y class="popup-content">
<text class="popup-title">{{ popupTitle }}</text>
<text class="popup-text">{{ popupContent }}</text>
</scroll-view>
<button class="popup-btn" @click="close">
我已知晓 / Acknowledge
</button>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { projectInfo } from '@/config';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const checked = ref(props.modelValue)
const popup = ref(null)
const popupType = ref('')
watch(
() => props.modelValue,
val => (checked.value = val)
)
const onChange = e => {
checked.value = e.detail.value.length > 0
emit('update:modelValue', checked.value)
}
const open = type => {
popupType.value = type
popup.value.open()
}
const close = () => {
popup.value.close()
}
const popupTitle = computed(() =>
popupType.value === 'service'
? '用户服务协议 / User Service Agreement'
: '隐私政策 / Privacy Policy'
)
const popupContent = computed(() => {
if (popupType.value === 'service') {
return `
用户服务协议
User Service Agreement
协议的确认与接受
欢迎您使用由${projectInfo.name}运营的仓库租赁小程序
您在注册登录或使用本小程序服务前应仔细阅读并充分理解本协议
您勾选同意或实际使用服务的行为即视为您已阅读理解并同意本协议全部内容
Welcome to the warehouse rental mini program operated by Company Name.
By registering, logging in, or using the service, you acknowledge that you have read,
understood, and agreed to this Agreement.
服务内容
本小程序向用户提供仓库租赁及相关服务包括但不限于
1. 仓库信息展示与查询
2. 仓库租赁订单的创建管理与履行
3. 用户身份认证个人/企业
4. 合同签署账单管理及费用结算
This mini program provides warehouse rental services, including but not limited to
warehouse information display, order management, identity verification, and contract execution.
用户身份/企业认证
为保障交易安全及符合法律法规要求用户需根据页面提示提供真实准确完整的信息
如信息不真实或不完整我们有权拒绝或终止相关服务
Users are required to provide true, accurate, and complete information for identity verification.
We reserve the right to refuse or terminate services if false information is provided.
用户权利与义务
用户应妥善保管账户信息不得转让出租或出借账户
用户不得利用本服务从事违法或侵害他人合法权益的行为
Users shall properly safeguard their account information and shall not engage in illegal activities.
协议变更与终止
我们有权依法对本协议进行修订并在小程序内进行公示
用户继续使用服务即视为接受修订后的协议
We reserve the right to amend this Agreement with notice provided within the mini program.
法律适用与争议解决
本协议适用中华人民共和国法律
因本协议产生的争议应提交本公司所在地有管辖权的人民法院解决
This Agreement shall be governed by the laws of the Peoples Republic of China.
`
}
return `
隐私政策
Privacy Policy
个人信息的收集
在您使用本小程序过程中我们可能收集以下信息
- 姓名
- 证件类型及证件号码
- 证件照片仅用于实名认证核验
- 手机号码
- 紧急联系人信息如您主动填写
- 企业名称
- 营业执照照片仅用于企业认证核验
- 营业执照号码仅用于企业认证核验
We may collect your name, ID type and number, ID photos, phone number,
emergency contact information (if provided), and order details.
信息的使用目的
上述信息仅用于
1. 用户身份认证
2. 仓库租赁服务的提供与履行
3. 合同签署与合规审查
4. 客户服务与风险控制
The collected information is used solely for identity verification,
service fulfillment, contract execution, and customer support.
信息的存储与保护
我们将采取加密权限控制等安全措施保护您的个人信息
防止未经授权的访问披露或滥用
We adopt industry-standard security measures to protect your personal information.
信息的保存期限
您的个人信息仅在实现服务目的所必需的期限内保存
超过期限后将依法删除或匿名化处理
Personal information is retained only as long as necessary and will be deleted or anonymized thereafter.
信息的共享与披露
未经您的明确同意我们不会向任何第三方共享或披露您的个人信息
法律法规另有规定的除外
We do not share or disclose your personal information without your consent,
except as required by law.
用户权利
您有权依法查询更正或删除您的个人信息
如需行使相关权利请通过客服与我们联系
You have the right to access, correct, or delete your personal information.
Please contact us via Customer Support Contact.
未成年人保护
本服务主要面向具备完全民事行为能力的用户
This service is intended for users with full legal capacity.
`
})
</script>
<style scoped>
.agreement-wrapper {
margin-top: 24rpx;
font-size: 26rpx;
color: #666;
}
.agreement-label {
display: flex;
align-items: flex-start;
}
.agreement-text {
margin-left: 12rpx;
line-height: 1.6;
}
.link {
color: #f7b500;
}
.en-tip {
display: block;
font-size: 22rpx;
color: #999;
margin-top: 6rpx;
}
.popup-box {
width: 680rpx;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
}
.popup-content {
max-height: 720rpx;
}
.popup-title {
display: block;
font-size: 30rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.popup-text {
font-size: 26rpx;
line-height: 1.6;
white-space: pre-wrap;
}
.popup-btn {
margin-top: 20rpx;
background-color: #f7b500;
color: #fff;
}
</style>

View File

@ -0,0 +1,158 @@
<template>
<view class="invite-wrap">
<myPopup v-model="modelValue" mode="bottom" bgColor="none"
customStyle="max-height: 70vh; border-radius: 15px 15px 0 0;" :closeOnClickOverlay="true">
<view class="inner-wrap">
<view class="title">{{ $t("referrerInfo.inviteRecord") }}</view>
<view class="close-icon" @click="closeShow">
<uv-icon name="close" size="10" :color="themeInfo.activeColor"></uv-icon>
</view>
<view class="top-nav">
<view class="label">{{ $t("referrerInfo.inviteUserName") }}</view>
<view class="label">{{ $t("referrerInfo.invitePhone") }}</view>
<view class="label">{{ $t("referrerInfo.registrationTime") }}</view>
</view>
<view class="content-wrap">
<view class="list-wrap" v-if="state.list.length">
<view class="item-wrap" v-for="(item, index) in state.list" :key="index">
<view class="label">{{ item.customerName }}</view>
<view class="label">{{ item.phone }}</view>
<view class="label">{{ item.createTime }}</view>
</view>
</view>
<view class="empty-wrap" v-else>
<view>{{ $t("referrerInfo.inviteEmpty") }}</view>
</view>
</view>
</view>
</myPopup>
</view>
</template>
<script setup>
import myPopup from './myPopup.vue';
import { reactive, watch, computed } from 'vue';
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import { getClientCustomerApi } from "@/Apis/clientCustomer.js";
const clientCustomerApi = getClientCustomerApi();
// v-model
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
});
const emit = defineEmits(['update:modelValue']);
const state = reactive({
list: []
});
const modelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
watch(() => modelValue.value, (value) => {
value && getRecommend()
});
const closeShow = () => {
modelValue.value = false;
}
const getRecommend = () => {
clientCustomerApi.GetMediatorUpUserList().then((res) => {
if (res.code === 200) {
state.list = res.data;
}
});
}
</script>
<style lang="scss" scoped>
.invite-wrap {
.inner-wrap {
position: relative;
.title {
padding: 40rpx 0;
font-size: 28rpx;
color: #FFFFFF;
text-align: center;
background: var(--main-color);
}
.close-icon {
position: absolute;
right: 30rpx;
top: 30rpx;
padding: 6rpx;
border-radius: 50%;
background: rgba($color: #FFFFFF, $alpha: 0.6);
}
.top-nav {
display: flex;
padding: 16rpx 0;
background: var(--right-linear);
.label {
width: 33%;
font-weight: bold;
font-size: 28rpx;
text-align: center;
color: var(--text-color);
}
}
.content-wrap {
padding: 20rpx 0 40rpx;
background: #F5F5EF;
min-height: 400rpx;
max-height: 600rpx;
overflow: auto;
.list-wrap {
.item-wrap {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
.label {
width: 33%;
font-weight: bold;
font-size: 30rpx;
text-align: center;
color: var(--main-color);
}
&:nth-child(odd) {
background: rgba(216, 216, 216, 0.4);
}
}
}
.empty-wrap {
padding: 30rpx 0;
margin: 20rpx 30rpx 0;
text-align: center;
font-size: 24rpx;
font-weight: bold;
border-radius: 16rpx;
color: var(--text-color);
background: var(--main-color);
}
}
}
}
</style>

820
components/coupon.vue Normal file
View File

@ -0,0 +1,820 @@
<template>
<view class="coupon" :class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]">
<myPopup v-model="modelValue" mode='bottom' bgColor='none' customStyle='height:70vh;border-radius: 18px 18px 0 0;' :closeOnClickOverlay='true'>
<view class="couponContent">
<view class="header">
<view class="line"></view>
<view class="title">
{{ $t('coupon.coupon') }} {{dataList.length + unusableDataList.length}}
</view>
</view>
<view class="content">
<view class="ortherCoupon" v-if="isKingKong && props.siteData?.siteId">
<view class="" v-if="storeState.userInfo?.phone">
<uv-divider :text="$t('coupon.meituanOrdazhongdianpingCoupon')"></uv-divider>
</view>
<view v-else>
<button @click="bindPhonePopup.open()"> {{ $t('coupon.queryMeituanDazhongdianpingCoupon') }} </button>
</view>
</view>
<view class="couponBox" v-if="isKingKong">
<view class="couponUl couponUl2">
<view v-if="state.ortherCouponLoading" style="display: flex;justify-content: center;">
<uv-loading-icon></uv-loading-icon>
</view>
<uv-checkbox-group style="width: 100%;" v-model="state.checkboxOrtherCouponValue" @change="ortherCouponChange">
<template v-for="(item,index) in state.ortherCoupon" :key="index">
<view class="couponLi">
<view class="left">
<view class="name">
{{ item.title }}
</view>
<view class="desc">
{{ $t('coupon.validityPeriod') }}{{ item.endTime.substr(0,10) }}
</view>
</view>
<view class="right">
<view style="display: flex;justify-content: center;margin-top: 4px;">
<uv-checkbox
:key="index"
:name="item.number"
@click.stop=""
></uv-checkbox>
<!-- <button class="btn" 0">
{{ $t('coupon.apply') }}
</button> -->
</view>
</view>
</view>
</template>
</uv-checkbox-group>
</view>
<uv-divider></uv-divider>
</view>
<view class="couponBox">
<view class="couponUl">
<view class="couponLi">
<view class="left">
<view class="name">
{{ $t('coupon.coupon') }}
</view>
<view class="desc">
{{ $t('coupon.useTips') }}
</view>
<view class="input">
<input v-model="couponCode" :placeholder="$t('coupon.enterCode')"></input>
</view>
</view>
<view class="right">
<view class="desc">
{{ $t('coupon.storewide') }}
</view>
<view class="desc">
{{ $t('coupon.limitedtimeoffer') }}
</view>
<view>
<button class="btn" @click="getCouponCode">
{{ $t("coupon.redeemNow") }}
</button>
</view>
</view>
</view>
</view>
<view class="couponUl couponUl2">
<uv-checkbox-group style="width: 100%;" v-model="state.checkboxCouponValue" @change="couponChange">
<view class="couponLi" v-for="(item,index) in dataList" :key="index" @click="chooseCoupon(item)" :style="{opacity:disabledFunc(item)?0.5:1}">
<view class="left">
<view class="name">
{{ item.couponCode }} {{ couponTitle(item) }}
<!-- <text style="letter-spacing: 0;">{{ item.discountLimit }}</text> -->
</view>
<view class="desc">
{{ $t('coupon.validityPeriod') }}{{ item.startDate.substr(0,10) }} ~ {{ item.endDate.substr(0,10)}}
</view>
<view class="desc">
{{ $t('coupon.instructions') }}{{ couponDesc(item) }} {{ item.siteName.length ?`${item.siteName.toString()}`: $t('coupon.storewide')}} {{ item.renewUsable?$t('coupon.renewable'):$t('coupon.noRenewable') }} {{ item.unitTypeName.length ? `(${item.unitTypeName.toString()})` : `(${$t("coupon.all")})`}}
</view>
</view>
<view class="right">
<view class="desc">
{{ item.siteName.length ?`${item.siteName.length>1?$t('coupon.multiStoreUse'):item.siteName[0]}`: $t('coupon.storewide') }}
</view>
<view class="desc">
{{ $t('coupon.limitedtimeoffer') }}
</view>
<view style="display: flex;justify-content: center;margin-top: 4px;">
<uv-checkbox
v-if="props.siteData?.lockerId"
:key="index"
:name="item.couponDispositionId"
:disabled="disabledFunc(item)"
@click.stop=""
></uv-checkbox>
<!-- <button class="btn" :disabled="props.disabledFunc(item)" @click="chooseCoupon(item)">
{{ $t('coupon.apply') }}
</button> -->
</view>
</view>
</view>
</uv-checkbox-group>
</view>
</view>
<view class="unusableCoupons couponUl">
<view v-if="unusableDataList.length" @click="state.showUnusableCoupons = !state.showUnusableCoupons" class="showUnusableCoupons">显示不可用优惠卷>></view>
<uv-divider v-if="unusableDataList.length && state.showUnusableCoupons" :text="$t('coupon.unusableCoupons')"></uv-divider>
<view v-show="state.showUnusableCoupons" class="couponLi" v-for="(item,index) in unusableDataList" :key="index">
<view class="left">
<view class="name">
{{ item.couponCode }} {{ couponTitle(item) }}
<!-- <text style="letter-spacing: 0;">{{ item.discountLimit }}</text> -->
</view>
<view class="desc">
{{ $t('coupon.validityPeriod') }}{{ item.startDate.substr(0,10) }} ~ {{ item.endDate.substr(0,10)}}
</view>
<view class="desc">
{{ $t('coupon.instructions') }}{{ couponDesc(item) }} {{ item.siteName.length ?`${item.siteName.toString()}`: $t('coupon.storewide')}} {{ item.renewUsable?$t('coupon.renewable'):$t('coupon.noRenewable') }} {{ item.unitTypeName.length ? `(${item.unitTypeName.toString()})` : `(${$t("coupon.all")})`}}
</view>
</view>
<view class="right">
<view class="desc">
{{ item.siteName.length ?`${item.siteName.length>1?$t('coupon.multiStoreUse'):item.siteName[0]}`: $t('coupon.storewide') }}
</view>
<view class="desc">
{{ $t('coupon.limitedtimeoffer') }}
</view>
<view>
<button class="btn" :disabled="true">
{{ $t('coupon.apply') }}
</button>
</view>
</view>
</view>
</view>
</view>
<view class="confirm" v-if="props.siteData?.lockerId">
<!-- @click="submit" -->
<button class="next-btn" @click="confirm">
{{ $t("common.confirm") }}
</button>
</view>
</view>
</myPopup>
<uv-popup
ref="bindPhonePopup"
customStyle="width: 80%; height: 400rpx; padding: 50rpx 0; border-radius: 32rpx; display: flex; flex-direction: column; justify-content: center; align-items: center;"
>
<text>{{ $t("common.bindPhone") }}</text>
<text style="padding: 0 40rpx; margin-top: 20rpx; text-align: center;"> {{ $t("common.bindPhoneUnlock") }}</text>
<!-- #ifdef MP-WEIXIN -->
<button
style="width: 80%; margin-top: 20px; background: #5BBC6B; color: #FFFFFF; line-height: 80rpx;"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
{{ $t("common.QuickBind") }}
</button>
<!-- #endif -->
</uv-popup>
</view>
</template>
<script setup>
import myPopup from './myPopup.vue';
import { ref,computed,watch } from 'vue';
import { couponApi } from '@/Apis/coupon.js';
import { useSiteApi } from "@/Apis/site.js";
import { AppId } from '@/config/index.js'
//
import { useI18n } from 'vue-i18n';
import { useMainStore } from "@/store/index.js";
import { useLoginApi } from "@/Apis/login.js";
import { useOrderApi } from "@/Apis/order.js";
const { t } = useI18n();
const getOrderApi = useOrderApi();
const getApi = couponApi();
const getSiteApi = useSiteApi();
const popup = ref();
const { themeInfo,storeState } = useMainStore();
const getLoginApi = useLoginApi();
// v-model
const props = defineProps({
modelValue:{
type:Boolean,
default:false
},
disabledFunc:{
type:Function,
default: ()=> false,
},
siteData:{
type:Object,
default:()=>{}
},
priceData:{
type:Object,
default:()=>{}
},
month:{
type:Number,
default:24
},
couponItem:{
type:Array,
default:()=>[]
},
couponOtherItem:{
type:Array,
default:()=>[]
},
isRenew:{
type:Boolean,
default:false
}
});
const isKingKong = (AppId === 'wxb20921dfdd0b94f4' || AppId === 'wx3c4ab696101d77d1') //
const bindPhonePopup = ref()
const couponCode = ref('');
const dataList = ref([]);
const unusableDataList = ref([]); //
const state = ref({
ortherCoupon:[],
ortherCouponLoading:false,
showUnusableCoupons: false,
checkboxCouponValue: [],
checkboxOrtherCouponValue: [], //
canComfirm: false,
})
const isShowOrtherCoupon = (item)=>{
return !props.couponOtherItem.map(x=>x.number).includes(item.number)
}
const emit = defineEmits(['close', 'confirm', 'update:modelValue','chooseCoupon']);
const modelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
})
// watch(() => modelValue.value, (value) => {
// state.value.ortherCoupon = []
// value && getDataList()
// })
watch(() => storeState.token, (value) => {
state.value.ortherCoupon = []
value && getDataList()
})
const open = ()=>{
modelValue.value = true;
state.value.checkboxCouponValue = props.couponItem.map(x=>x.couponDispositionId);
state.value.checkboxOrtherCouponValue = props.couponOtherItem.map(x=>x.number);
getDataList();
}
const close = ()=>{
modelValue.value = false;
}
//
const couponTitle = (item)=>{
let detail = ''
switch(item.couponType){
case 1:
detail = `${t('discountMomey',{discount:item.discountLimit})}`
break
case 2:
detail = `${t('couponDiscount',{percent: 100 - item.discountRange * 100,discount: (item.discountRange * 100) / 10,})}`
break
case 3:
detail = `${t('firstMonthRent',{discount:item.firstMonthAmount})}`
break
case 4:
detail = `${t('couponDiscount',{percent: 100 - item.monthDiscount * 100,discount: (item.monthDiscount * 100) / 10})}`
break
case 5:
detail = `${t('freeMonth',{discount:item.freeMonth})}`
break
case 6:
detail = `${t('BonusMonth',{discount:item.freeMonth})}`
break
default:
detail = `${t('discountMomey',{discount:item.discountLimit})}`
break
}
return `${detail}`
}
const couponDesc = (item)=>{
let detail = ''
switch(item.couponType){
case 1:
detail = `${t('requiredMomey',{momey: item.satisfyAmount})}`
break
case 2:
detail = `${t('requiredMomey',{momey: item.satisfyAmount})}`
break
case 3:
detail = `${t('fullMonths',{count: item.fullMonth})}`
break
case 4:
detail = `${t('fullMonths',{count: item.fullMonth})}`
break
case 5:
detail = `${t('fullMonths',{count: item.fullMonth})}`
break
case 6:
detail = `${t('fullMonths',{count: item.fullMonth})}`
break
default:
detail = `${t('requiredMomey',{momey: item.satisfyAmount})}`
break
}
return `${detail}`
}
//
function splitUnitTypeIdsByCoupon(coupon, siteId, unitTypeId) {
const usable = []
const unusable = []
coupon.forEach((item) => {
const siteUsable = item.siteIds.length === 0 || item.siteIds.includes(siteId)
const unitUsable = item.unitTypeIds.length === 0 || item.unitTypeIds.includes(unitTypeId)
if (siteUsable && unitUsable) {
usable.push(item)
} else {
unusable.push(item)
}
})
return { usable, unusable }
}
const getCouponCode = ()=>{
if(couponCode.value !== ''){
uni.showLoading({
mark: true
})
getApi.DrawDownCoupon({couponCode:couponCode.value}).then(res=>{
if(res.code == 200){
uni.showToast({
title: t('coupon.redemptionuccessful'),
icon: 'none'
})
setTimeout(()=>{
getDataList()
},1000)
}else{
uni.showToast({
title: res.msg,
icon: 'none'
})
}
}).finally(()=>{
uni.hideLoading()
})
}
}
const getDataList = ()=>{
uni.showLoading()
getApi.GetCouponList({pageIndex:1,pageSize:10000}).then(res=>{
uni.hideLoading()
if(res.code == 200){
dataList.value = res.data.result;
res.data.result.forEach(item=>{
item.startDate = item.startDate.replace(/-/g,'/')
item.endDate = item.endDate.replace(/-/g,'/')
})
if(props.siteData?.siteId){
const { usable, unusable} = splitUnitTypeIdsByCoupon(res.data.result, props.siteData?.siteId, props.siteData?.unitTypeId)
dataList.value = usable;
unusableDataList.value = unusable;
}else{
dataList.value = res.data.result;
unusableDataList.value = [];
}
getOtherCoupon()
}else{
setTimeout(()=>{
modelValue.value = false;
},500)
}
}).finally(()=>{
uni.hideLoading()
})
}
const chooseCoupon = (item)=>{
if(disabledFunc(item)){
uni.showToast({
title: t('coupon.currentConditionsNotMet'),
icon: 'none'
})
return
}
if(state.value.checkboxCouponValue.includes(item.couponDispositionId)){
state.value.checkboxCouponValue = state.value.checkboxCouponValue.filter(x=>x !== item.couponDispositionId)
}else{
state.value.checkboxCouponValue.push(item.couponDispositionId)
}
if(!props.siteData){
emit('chooseCoupon',item)
modelValue.value = false;
}
}
const chooseOtherCoupon = (item)=>{
emit('chooseOtherCoupon',item)
modelValue.value = false;
}
//
const getOtherCoupon = ()=>{
const phone = storeState.userInfo?.phone;
const siteId = props.siteData?.siteId;
if(siteId && phone && isKingKong){
state.value.ortherCouponLoading = true;
getApi.GetMeiTuanCodeByPhone({phone,siteId}).then(res=>{
state.value.ortherCouponLoading = false;
if(res.code == 200){
state.value.ortherCoupon = res.data;
}
// state.value.ortherCoupon = [
// {
// number: "NO20240711001",
// endTime: "2025-08-31 23:59:59",
// platform: "",
// title: "",
// price: "88.00",
// dealId: "D123456",
// dealGroupId: "G987654",
// marketPrice: "128.00",
// receiptBeginDate: "2025-08-01",
// receiptEndDate: "2025-08-31"
// },
// {
// number: "NO20240711002",
// endTime: "2025-09-15 23:59:59",
// platform: "",
// title: "",
// price: "168.00",
// dealId: "D654321",
// dealGroupId: "G123789",
// marketPrice: "218.00",
// receiptBeginDate: "2025-09-01",
// receiptEndDate: "2025-09-15"
// }
// ];
})
}
return
}
const getPhoneNumber = (e) => {
uni.showLoading();
if (e.detail.code) {
getLoginApi.GetPhoneNumber({code:e.detail.code}).then((res) => {
if (res.code == 200) {
uni.hideLoading();
uni.showToast({
title: "获取手机号成功",
icon: "none",
duration: 2000,
});
bindPhonePopup.value.close();
storeState.userInfo.phone = res.data;
getOtherCoupon();
} else {
uni.hideLoading();
uni.showToast({
title: "获取手机号失败",
icon: "none",
duration: 2000,
});
}
});
} else {
uni.hideLoading();
uni.showToast({
title: "获取手机号失败",
icon: "none",
duration: 2000,
});
}
}
//
const disabledFunc = (item) => {
if(!props.siteData?.siteId) return false
if (!item) {
return true;
}
//
const couponItem = dataList.value.filter(
(x) => state.value.checkboxCouponValue.includes(x.couponDispositionId)
);
// false
if(state.value.checkboxCouponValue.includes(item.couponDispositionId)) return false;
//
const isExistSameType = couponItem.some(
(x) => x.couponType === item.couponType
);
if(isExistSameType) return true;
// item.siteIds
const isSite = item.siteIds.length
? item.siteIds.includes(props.siteData.siteId)
: true;
if(!isSite) return true;
const isUnit = item.unitTypeIds.length
? item.unitTypeIds.includes(props.siteData.unitTypeId)
: true;
if(!isUnit) return true;
let isPrice = item.satisfyAmount
? props.priceData.discountExpense >= item.satisfyAmount
: true;
if([3,4,5,6].includes(item.couponType)){
isPrice = props.month>=item.fullMonth
}
if(!isPrice) return true;
// Date
const start = new Date(item.startDate);
const end = new Date(item.endDate);
//
const now = new Date();
//
const isDate = now >= start && now <= end;
if(!isDate) return true;
//
const canAdd = canAddValue(couponItem.map(x=>x.couponType),item.couponType)
if(!canAdd) return true;
//
let canReNew = true;
if(props.isRenew){
canReNew = item.renewUsable
}
if(!canReNew) return true;
return !(isSite && isUnit && isPrice && isDate&& canAdd && canReNew);
};
function canAddValue(arr, value) {
//
const allowedNumbers = new Set([1, 3, 4]);
//
if (arr.length === 0) {
return true;
}
// 134
const hasInvalidNumber = arr.some(num => !allowedNumbers.has(num));
if (hasInvalidNumber) {
return false; // 134
}
// 134
if (allowedNumbers.has(value) && !arr.includes(value)) {
return true;
}
//
return false;
}
const ortherCouponChange = (e)=>{
const couponItem = dataList.value.filter(
(x) => state.value.checkboxCouponValue.includes(x.couponDispositionId)
);
const couponOtherItem = state.value.ortherCoupon.filter(
(x) => state.value.checkboxOrtherCouponValue.includes(x.number)
);
getLockerExpense(couponItem,couponOtherItem)
}
const couponChange = (e)=>{
const couponItem = dataList.value.filter(
(x) => state.value.checkboxCouponValue.includes(x.couponDispositionId)
);
const couponOtherItem = state.value.ortherCoupon.filter(
(x) => state.value.checkboxOrtherCouponValue.includes(x.number)
);
getLockerExpense(couponItem,couponOtherItem)
}
const getLockerExpense = async (couponItem,couponOtherItem) => {
uni.showLoading({
mask: true,
});
const LockerExpenseApi = props.isRenew ? getOrderApi.ContinuationOrderPricePost : getSiteApi.GetLockerExpense;
await LockerExpenseApi({
lockerId: props.siteData.lockerId,
orderId: props.siteData.orderId,
month: props.month,
couponIds: couponItem.map((item) => item.couponDispositionId),
serialNumber: couponOtherItem.map(x => {
return {
dealGroupId: x.dealGroupId,
number: x.number,
marketPrice: x.marketPrice,
purchasePrice: x.price
}
})
})
.then((res) => {
uni.hideLoading();
if (res.code === 200) {
state.value.canComfirm = true
state.value.feeData = res.data;
}else{
state.value.canComfirm = false
}
if(res.code === 1002){
uni.showModal({
title: t('common.title'),
content: t('common.ORDER_AMOUNT_ERROR'),
showCancel: false,
success: function () {},
});
}
});
};
const confirm = async ()=>{
const couponItem = dataList.value.filter(
(x) => state.value.checkboxCouponValue.includes(x.couponDispositionId)
);
const couponOtherItem = state.value.ortherCoupon.filter(
(x) => state.value.checkboxOrtherCouponValue.includes(x.number)
);
await getLockerExpense(couponItem,couponOtherItem)
if(!state.value.canComfirm) return
emit('confirm',{couponItem,couponOtherItem})
modelValue.value = false;
}
defineExpose({
getOtherCoupon,
open,
close
})
</script>
<style lang="scss" scoped>
.coupon{
.couponContent{
.confirm{
height: 100rpx;
width: 100%;
background-color: #fff;
display: flex;
justify-content: center;
.next-btn{
position: absolute;
bottom:30upx;
width: 90%;
padding: 4px 0;
font-size: 28rpx;
font-weight: bold;
color: var(--text-color);
background: var(--active-color);
border-radius: 16rpx;
}
}
height: 100%;
background-color: #fff;
border-radius: 18upx 18upx 0 0;
padding-bottom: 40upx;
display: flex;
flex-direction: column;
.header{
height: 100rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.line{
width: 80rpx;
height: 10rpx;
background-color:#CBCDCC80;
margin: 0 auto;
border-radius: 99rpx;
}
.title{
font-size: 36rpx;
align-self: self-start;
margin-left: 40rpx;
}
}
.content{
flex: 1;
padding: 0 20upx 100upx 20upx;
overflow-y: auto;
display: flex;
flex-direction: column;
position: relative;
.couponUl{
.couponLi{
position: relative;
display: flex;
padding: 40upx;
width: 100%;
margin: 20upx 0;
border-radius: 18upx;
color: #fff;
background: linear-gradient(90deg, var(--left-linear), var(--right-linear));
box-shadow: 0px -2px 5px 0px rgba(0, 0, 0, 0.13);
font-size: 22upx;
&::after{
content: "";
position: absolute;
top: 2px;
right: -7px;
width: 14px; /* 调整宽度 */
height: 100%; /* 调整高度以适应圆形的垂直排列 */
background: radial-gradient(#ffffff 0px, #ffffff 5px, transparent 5px, transparent);
background-size: 14px 14px; /* 保持圆形的大小 */
background-repeat: repeat-y; /* 垂直重复 */
z-index: 9;
}
.left{
width: 68%;
display: flex;
flex-direction: column;
position: relative;
.name{
font-size: 36upx;
letter-spacing: 4upx;
font-weight: bold;
}
.desc{
margin-top: 10upx;
}
.input{
background-color: var(--bg-popup);
text-align: center;
width: 220upx;
border-radius: 12upx;
color: var(--text-color);
margin-top: 20upx;
}
&::before{
content: ' ';
display: block;
height: 32upx;
width: 32upx;
border-radius: 99rpx;
background-color: #fff;
position: absolute;
bottom: 0;
top: -56upx;
right: -18upx;
}
&::after{
content: ' ';
display: block;
height: 32upx;
width: 32upx;
border-radius: 99rpx;
background-color: #fff;
position: absolute;
bottom: -56upx;
right: -18upx;
}
border-right: 1px solid #fff;
}
.right{
width: 32%;
display: flex;
position: relative;
flex-direction: column;
justify-content: center;
padding-left: 5%;
.btn{
border-radius: 99rpx;
background-color: #fff;
color: var(--stress-color2);
font-size: 24upx;
line-height: 60upx;
margin-top: 30upx;
font-weight: bold;
&[disabled]{
opacity: 0.5;
}
}
.desc{
margin-top: 10upx;
text-align: center;
letter-spacing: 4upx;
}
}
}
}
}
}
}
</style>

163
components/inviteDetail.vue Normal file
View File

@ -0,0 +1,163 @@
<template>
<view class="invite-wrap">
<myPopup v-model="modelValue" mode="bottom" bgColor="none" customStyle="max-height: 70vh; border-radius: 15px 15px 0 0;"
:closeOnClickOverlay='true'>
<view class="inner-wrap">
<view class="title">{{ $t('inviteDetail.title') }}</view>
<view class="close-icon" @click="closeShow">
<uv-icon name="close" size="10" :color="themeInfo.activeColor"></uv-icon>
</view>
<view class="top-nav">
<view class="label">{{ $t('inviteDetail.Username') }}</view>
<view class="label">{{ $t('inviteDetail.Registration Date') }}</view>
<view class="label">{{ $t('inviteDetail.Status') }}</view>
</view>
<view class="content-wrap">
<view class="list-wrap" v-if="state.list.length">
<view class="item-wrap" v-for="item in state.list">
<view class="label">{{ item.name }}</view>
<view class="label">{{ item.createTime }}</view>
<view class="label">{{ item.status }}</view>
</view>
</view>
<view class="empty-wrap" v-else>
<view>{{ $t('inviteDetail.No invitation') }}</view>
</view>
<button open-type="share" class="share-btn">{{ $t('inviteDetail.Share Invitation') }}</button>
</view>
</view>
</myPopup>
</view>
</template>
<script setup>
import myPopup from './myPopup.vue';
import { reactive, watch, computed } from 'vue';
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import { useRecommend } from "@/Apis/recommend.js";
const getApi = useRecommend();
// v-model
const props = defineProps({
modelValue:{
type:Boolean,
default:false
},
});
const emit = defineEmits(['update:modelValue']);
const modelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
})
watch(() => modelValue.value, (value) => {
value && getRecommend()
})
const closeShow = () => {
modelValue.value = false;
}
const getRecommend = () => {
getApi.GetRecommend().then((res) => {
if(res.code === 200){
state.list = res.data;
}
});
}
const state = reactive({
list: []
});
</script>
<style lang="scss" scoped>
.invite-wrap {
.inner-wrap {
position: relative;
.title {
padding: 40rpx 0;
font-size: 28rpx;
color: #FFFFFF;
text-align: center;
background: var(--main-color);
}
.close-icon {
position: absolute;
right: 30rpx;
top: 30rpx;
padding: 6rpx;
border-radius: 50%;
background: rgba($color: #FFFFFF, $alpha: 0.6);
}
.top-nav {
display: flex;
padding: 16rpx 0;
background: var(--main-color);
.label {
width: 33.33%;
font-weight: bold;
font-size: 28rpx;
text-align: center;
color: var(--text-color);
}
}
.content-wrap {
padding: 20rpx 0 40rpx;
background: #FFFFFF;
.list-wrap {
max-height: 460rpx;
overflow: auto;
.item-wrap {
display: flex;
padding: 16rpx 0;
.label {
width: 33.33%;
font-weight: bold;
font-size: 30rpx;
text-align: center;
color: var(--text-color);
}
}
}
.empty-wrap {
padding: 30rpx 0;
margin: 20rpx 30rpx 0;
text-align: center;
font-size: 24rpx;
font-weight: bold;
border-radius: 16rpx;
background: var(--main-color);
.en {
margin-top: 20rpx;
font-size: 16rpx;
}
}
.share-btn {
margin: 40rpx 40rpx 0;
text-align: center;
font-weight: bold;
font-size: 28rpx;
border-radius: 40rpx;
color: var(--text-color);
background: var(--main-color);
}
}
}
}
</style>

124
components/my-dropdown.vue Normal file
View File

@ -0,0 +1,124 @@
<!--
原生 uni-app 下拉组件不依赖 Element UI
特点
- 点击展开下拉菜单
- 选择后关闭
- v-model 支持
- 简洁轻量
用法
<uni-native-dropdown v-model="value" :items="['编辑','删除','更多']" />
<uni-native-dropdown
v-model="selected"
:items="[
{ label: '编辑', value: 'edit' },
{ label: '删除', value: 'del' }
]
placeholder="请选择操作"
/>
-->
<template>
<view class="dropdown-container">
<!-- 触发区域 -->
<view @click="toggle">
<slot name="trigger" :selected="selectedLabel">
<view class="default-trigger">
<text>{{ selectedLabel }}</text>
<view class="arrow"></view>
</view>
</slot>
</view>
<!-- 下拉菜单 -->
<view v-if="open" class="dropdown-menu">
<view
class="dropdown-item"
v-for="(item, i) in normalizedItems"
:key="i"
@click="selectItem(item)"
>
{{ item.label }}
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UniNativeDropdown',
props: {
modelValue: { type: [String, Number, Object], default: '' },
items: { type: Array, default: () => [] },
placeholder: { type: String, default: '请选择' }
},
data() {
return { open: false }
},
computed: {
normalizedItems() {
// string {label, value}
return this.items.map(i =>
typeof i === 'string' ? { label: i, value: i } : i
)
},
selectedLabel() {
const item = this.normalizedItems.find(i => i.value === this.modelValue)
return item ? item.label : this.placeholder
}
},
methods: {
toggle() {
this.open = !this.open
},
selectItem(item) {
this.$emit('update:modelValue', item.value)
this.$emit('change', item)
this.open = false
}
}
}
</script>
<style scoped>
.dropdown-container {
position: relative;
width: auto;
}
.dropdown-trigger {
border: 1px solid #ccc;
padding: 20rpx;
border-radius: 8rpx;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
}
.arrow {
font-size: 28rpx;
margin-left: 10rpx;
transform: scaleY(0.8);
display: inline-block; /* 重要 */
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
width: 100%;
background: #fff;
border: 1px solid #ccc;
border-radius: 8rpx;
margin-top: 6rpx;
z-index: 999;
}
.dropdown-item {
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.dropdown-item:last-child {
border-bottom: 0;
}
.dropdown-item:active {
background: #f2f2f2;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<uni-tabbar class="myCustomTabbar uni-tabbar" :style="{ flexDirection: direction === 'horizontal' ? 'row' : 'column' }">
<div
class="uni-tabbar__item"
v-for="(item, index) in tabItems"
:key="index"
@click="handleTabItemTap(item)"
:class="{ 'call-phone-item': item.phoneIcon }"
>
<div class="uni-tabbar__bd">
<div class="uni-tabbar__icon">
<svg v-if="index === 0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="18" height="18.733154296875" viewBox="0 0 18 18.733154296875"><path d="M18,17.733154C18,18.285454,17.552299,18.733154,17,18.733154L1,18.733154C0.44772005,18.733154,0,18.285454,0,17.733154L0,7.2222252C0,6.9136353,0.14246988,6.6223249,0.38606,6.4328756L8.3860998,0.21064496C8.7472,-0.070214987,9.2528,-0.070214987,9.6139002,0.21064496L17.613899,6.4328756C17.8575,6.6223249,18,6.9136353,18,7.2222252L18,17.733154ZM16,16.733154L16,7.7113056L9,2.266865L2,7.7113056L2,16.733154L16,16.733154Z" :fill="selected === 0 ? '#fb322e' : '#000000'" fill-opacity="1" style="mix-blend-mode:passthrough"/></svg>
<svg v-if="index === 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="18" height="20" viewBox="0 0 18 20"><path d="M17,20L1,20C0.44772005,20,0,19.552299,0,19L0,1C0,0.44772005,0.44772005,0,1,0L17,0C17.552299,0,18,0.44772005,18,1L18,19C18,19.552299,17.552299,20,17,20ZM16,18L16,2L2,2L2,18L16,18ZM5,5L13,5L13,7L5,7L5,5ZM5,9L13,9L13,11L5,11L5,9ZM5,13L10,13L10,15L5,15L5,13Z" :fill="selected === 1 ? '#fb322e' : '#000000'" fill-opacity="1" style="mix-blend-mode:passthrough"/></svg>
<svg v-if="index === 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="21" viewBox="0 0 16 21"><path d="M16,21L14,21L14,19C14,17.3431,12.6569,16,11,16L5,16C3.3431501,16,2,17.3431,2,19L2,21L0,21L0,19C0,16.2386,2.2385802,14,5,14L11,14C13.7614,14,16,16.2386,16,19L16,21ZM8,12C4.6862898,12,2,9.3136997,2,6C2,2.68629,4.6862898,0,8,0C11.3137,0,14,2.68629,14,6C14,9.3136997,11.3137,12,8,12ZM8,10C10.2091,10,12,8.2091398,12,6C12,3.7908602,10.2091,2,8,2C5.7908602,2,4,3.7908602,4,6C4,8.2091398,5.7908602,10,8,10Z" :fill="selected === 2 ? '#fb322e' : '#000000'" fill-opacity="1" style="mix-blend-mode:passthrough"/></svg>
<!-- <uv-icon v-if="showIcon" :name="item.icon" custom-prefix="custom-icon" :size="item.size || 20" :color="themeInfo.iconColor"></uv-icon> -->
</div>
<div class="uni-tabbar__label" :style="{ fontSize: '10px',color: selected === index ? '#fb322e' : '#000000' }">{{ item.label }}</div>
</div>
</div>
</uni-tabbar>
</template>
<script setup>
import { ref, computed } from 'vue';
import { defineProps, defineEmits } from 'vue';
import { isXiaohongshu } from "@/config/index.js";
//
import { useMainStore } from "@/store/index.js"
const { themeInfo } = useMainStore();
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
direction: {
type: String,
default: 'horizontal',
},
showIcon: {
type: Boolean,
default: true,
},
selected: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['onTabItemTap']);
const tabItems = computed(() => {
if (isXiaohongshu) {
return [
{ label: t('tabbar.home'), icon: 'home1', index:0, pagePath:'pages/index/index' },
{ label: '', icon: 'telephone', index:2, pagePath:'', size: 34, phoneIcon: true },
{ label: t('tabbar.book'), icon: 'book', index:1, pagePath:'pages/book/index'},
]
} else {
return [
{ label: t('tabbar.home'), icon: '/static/tabbar/home.svg', index:0, pagePath:'pages/index/index' },
// { label: t('tabbar.book'), icon: 'book', index:1, pagePath:'pages/book/index'},
// { label: '', icon: 'telephone', index:2, pagePath:'', size: 34, phoneIcon: true },
{ label: "订单", icon: '/static/tabbar/order.svg', index:3, pagePath:'pages/unlock/index' },
{ label: t('tabbar.personal'), icon: '/static/tabbar/user.svg', index:4, pagePath:'pages/personal/index' },
]
}
});
const handleTabItemTap = (index) => {
emit('onTabItemTap', index);
};
</script>
<style scoped lang="scss">
.myCustomTabbar {
display: flex;
}
.uni-tabbar{
box-sizing: border-box;
width: 100%;
z-index: 998;
}
.uni-tabbar__item{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex: 1;
font-size: 0;
padding: 0;
text-align: center;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.uni-tabbar__bd{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex: 1;
font-size: 0;
text-align: center;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
svg {
max-width: 100%;
height: 40rpx;
width: 40rpx;
}
</style>

210
components/myModal.vue Normal file
View File

@ -0,0 +1,210 @@
<template>
<view class="popup">
<uv-popup ref="popup" customStyle="width: 688rpx; height: auto; padding:32rpx;" round="16rpx"
:closeOnClickOverlay="false" :safeAreaInsetBottom="false">
<view class="modal-container">
<!-- 标题 -->
<view class="modal-title font32 fontb">
{{ props.title || $t('common.title') }}
</view>
<!-- 内容 -->
<view class="modal-text-wrap" v-if="props.content">
<text class="modal-text font28 fontb">
{{ props.content }}
</text>
</view>
<slot></slot>
<!-- 按钮 -->
<view class="modal-button">
<uv-button v-if="cancelShow" @click="closeModal" :customStyle="{
height: '86rpx',
lineHeight: '86rpx',
color: '#000',
fontSize: '32rpx',
}" shape="circle">
{{ props.cancelText || $t('common.cancel') }}
</uv-button>
<uv-button v-if="confirmShow" :customStyle="{
height: '86rpx',
background: '#FB322E',
lineHeight: '86rpx',
color: '#fff',
fontSize: '32rpx',
}" shape="circle" @click="confirm">
{{ props.confirmText || $t('common.confirm') }}
</uv-button>
<!-- <view
v-if="cancelShow"
class="modal-button-item modal-button-1"
@click="closeModal"
>
{{ props.cancelText || $t('common.cancel') }}
</view>
<view
v-if="confirmShow"
class="modal-button-item modal-button-2"
@click="confirm"
>
{{ props.confirmText || $t('common.confirm') }}
</view> -->
<slot name="affterBtn"></slot>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import { ref, computed, watch } from "vue";
const popup = ref()
const props = defineProps({
title: {
type: String,
default: ""
},
content: {
type: String,
default: ""
},
cancelText: {
type: String,
default: ""
},
cancelShow: {
type: Boolean,
default: true
},
confirmText: {
type: String,
default: ""
},
confirmShow: {
type: Boolean,
default: true
},
modelValue: {
type: Boolean,
default: false
},
noClose: {
type: Boolean,
default: false
}
})
const emit = defineEmits([
"close",
"confirm",
"update:modelValue"
])
const modelValue = computed({
get() {
return props.modelValue
},
set(value) {
emit("update:modelValue", value)
if (value) {
popup.value?.open()
} else {
popup.value?.close()
}
}
})
watch(
() => modelValue.value,
(val) => {
if (!popup.value) {
setTimeout(() => {
val ? popup.value?.open() : popup.value?.close()
}, 300)
} else {
val ? popup.value?.open() : popup.value?.close()
}
},
{
immediate: true
}
)
const closeModal = () => {
emit("close")
if (!props.noClose) {
modelValue.value = false
}
}
const confirm = () => {
emit("confirm")
if (!props.noClose) {
modelValue.value = false
}
}
</script>
<style lang="scss">
.modal-container {
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
}
/* 标题 */
.modal-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 36rpx;
color: #000000;
}
/* 文本外层 */
.modal-text-wrap {
width: 100%;
text-align: center;
margin-top: 20rpx;
}
/* 文本 */
.modal-text {
display: inline-block;
text-align: left;
// font-size: 34rpx;
// font-weight: 600;
// line-height: 48rpx;
padding: 20rpx 0;
max-width: 100%;
}
/* 按钮容器 */
.modal-button {
margin-top: 20rpx;
width: 100%;
min-height: 86rpx;
display: flex;
gap: 20rpx;
:deep(.uv-button-wrapper){
flex: 1;
}
}
</style>

172
components/myPopup.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<uv-popup
ref="popup"
:customStyle="props.customStyle"
:closeOnClickOverlay="props.closeOnClickOverlay"
@maskClick="maskClick"
:mode="props.mode"
:bgColor='props.bgColor'
:closeable="props.closeable"
@change="handleChange"
:safeAreaInsetBottom="false"
>
<slot></slot>
</uv-popup>
</template>
<script setup>
import { ref,watch,computed, nextTick } from 'vue';
const popup = ref();
// v-model
const props = defineProps({
modelValue:{
type:Boolean,
default:false
},
closeOnClickOverlay:{
type:Boolean,
default:false
},
mode:{
type:String,
default:'center'
},
customStyle:{
type:String,
default:'width: 90%; height: auto; padding:20rpx 0; border-radius: 32rpx;'
},
bgColor:{
type:String,
default:'#FFFFFF'
},
closeable:{
type:Boolean,
default:false
}
});
const handleChange = (e) => {
emit('update:modelValue', e.show);
}
const maskClick = () => {
modelValue.value = false;
}
const emit = defineEmits(['close', 'confirm', 'update:modelValue']);
const modelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
if(value){
popup.value.open()
}else{
popup.value.close()
}
}
})
watch(() => modelValue.value, (val) => {
//
if(!popup.value){
nextTick(()=>{
val?popup.value?.open():popup.value?.close();
})
}else{
val?popup.value?.open():popup.value?.close();
}
},{
immediate:true
});
</script>
<style lang="scss">
.modal-container {
// height: 700rpx;
// background: #000;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: column;
.modal-title {
width: 486rpx;
height: 42rpx;
text-align: center;
font-weight: bold;
font-size: 36rpx;
color: #000000;
position: relative;
// display: inline-block;
z-index: 1;
&::before{
position: absolute;
content: ' ';
width: 100%;
height: 14rpx;
background-color: var(--main-color);
border-radius: 100px;
display: block;
bottom: 0;
z-index: -1;
}
}
.modal-text {
width: 486rpx;
margin-top: 38rpx;
font-size: 26rpx;
font-weight: 600;
text-align: left;
line-height: 32rpx;
}
.upload {
margin-top: 30rpx;
height: 250rpx;
width: 100%;
// background: #000;
display: flex;
justify-content: center;
// overflow-x: scroll;
overflow-y: scroll;
// white-space: nowrap;
flex-wrap:nowrap;
// overflow: hidden;
}
.modal-button {
margin-top: 80rpx;
width: 527.22rpx;
height: 72rpx;
display: flex;
justify-content: space-between;
align-items: center;
& > .modal-button-1 {
width: 260rpx;
height: 72rpx;
background: #F4F3F3;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 700;
border-radius: 14rpx;
&:active {
background: #888;
}
}
& > .modal-button-2 {
width: 260rpx;
height: 72rpx;
background: var(--main-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 700;
color: #FBFBFB;
border-radius: 14rpx;
&:active {
background: #888;
}
}
}
}
</style>

142
components/myUpload.vue Normal file
View File

@ -0,0 +1,142 @@
<template>
<uv-upload
ref="fileListRef"
:fileList="props.modelValue"
name="1"
:width="props.width"
:height="props.height"
imageMode="aspectFit"
:maxCount="1"
:sizeType="['compressed']"
:previewFullImage="false"
:uploadText="props.uploadText"
@afterRead="afterRead"
@delete="deletePic"
@clickPreview="clickPreview"
>
<slot></slot>
</uv-upload>
</template>
<script setup>
import { ref} from "vue";
import { useOrderApi } from "@/Apis/order.js";
import { baseImageUrl,watermarkURL} from "@/config/index.js";
const getApi = useOrderApi();
// const fileList = ref([]);
const fileListRef = ref();
const imageList = ref([]);
const props = defineProps({
modelValue: Array,
previewFullImage: {
type: Boolean,
default: false,
},
addWatermark:{
type: Boolean,
default: false,
},
/* 新增 ↓↓↓ */
uploadText: {
type: String,
default: () => uni.$u?.t?.("unlock.uploadTip") || "Upload"
},
width: {
type: String,
default: "280rpx"
},
height: {
type: String,
default: "220rpx"
}
});
const emit = defineEmits(["update:modelValue"]);
const clickPreview = (event) => {
if(props.previewFullImage){
let url = '';
if (event.thumb) {
url = event.thumb;
} else {
const reg = /^(http|https):\/\//;
url = reg.test(event.url) ? event.url : baseImageUrl + event.url+(props.addWatermark?watermarkURL:'');
}
uni.previewImage({
urls: [url],
});
}
};
//
const deletePic = (event) => {
let fileList = [...props.modelValue];
fileList.splice(event.index, 1);
emit("update:modelValue", fileList);
};
//
async function UploaderImage(url) {
let url1 = "";
try {
const res = await getApi.UploaderImage({ filePath: url });
// const jsonstr = JSON.parse(res);
url1 = res.data+(props.addWatermark?watermarkURL:'');
} catch (error) {
//
console.error("UploaderImage error:", error);
throw error; //
}
return url1;
}
//
const afterRead = async (event) => {
let fileList = [...props.modelValue];
let lists = [].concat(event.file); //
let fileListLen = fileList.length;
//
lists.forEach((item) => {
//
item.thumb = null
fileList.push({
...item,
status: "uploading",
message: "上传中",
});
});
emit("update:modelValue", [...fileList]);
//
const uploadPromises = lists.map(async (item, index) => {
try {
const result = await UploaderImage(item.url);
fileList[fileListLen + index] = {
...item,
status: "success",
message: "",
thumb:baseImageUrl+result,
url: result,
};
} catch (error) {
fileList[fileListLen + index] = {
...item,
status: "failed",
message: "上传失败",
url: "",
};
}
});
//
await Promise.all(uploadPromises);
//
emit("update:modelValue", [...fileList]);
};
</script>
<style lang="scss">
::v-deep .uv-upload__deletable {
width: 20px !important;
height: 20px !important;
}
</style>

67
components/navBar.vue Normal file
View File

@ -0,0 +1,67 @@
<template>
<view class="back">
<view class="left" @click="backTo">
<uv-icon class="leftIcon" name="arrow-left" color="#000" blod size="32rpx"></uv-icon>
</view>
</view>
</template>
<script setup>
import { reactive, onMounted } from 'vue';
import { navigateBack,navbarHeightAndStatusBarHeight } from '@/utils/common.js';
import { useMainStore } from '@/store/index.js';
const { themeInfo } = useMainStore();
const backTo = ()=>{
navigateBack()
}
let state = reactive({
navHeight: 50,
statusBarHeight: 0,
});
onMounted(() => {
// #ifdef MP-WEIXIN || MP-XHS
const { tempHeight, navbarHeight, statusBarHeight } = navbarHeightAndStatusBarHeight();
state.navHeight = navbarHeight || tempHeight.navbarHeight;
state.statusBarHeight = statusBarHeight || tempHeight.statusBarHeight;
// #endif
});
</script>
<style lang="scss">
.back{
position: relative;
z-index: 99;
box-sizing: border-box;
height: 90rpx;
width: 100vw;
padding-left: 40rpx;
background-color: #FFF;
/* #ifdef APP-PLUS */
margin-top: --status-bar-height;
/* #endif */
display: flex;
align-items: center;
overflow: hidden;
margin-bottom: 20rpx;
.leftIcon span {
font-weight:bold;
}
.left{
width: 120rpx;
height: 100%;
display: flex;
align-items: center;
font-weight:bold;
::v-deep span {
font-weight:bold;
}
text {
font-weight:bold!important;
}
// transform: scale(1,1.4);
// transform-origin: 0 0;
}
}
</style>

29
components/noToken.vue Normal file
View File

@ -0,0 +1,29 @@
<template>
<view class="noToken" v-if="!token" @click="goLogin">
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app';
import { navigateTo } from '@/utils/navigateTo';
const goLogin = ()=>{
navigateTo('/pages/login/index')
}
let token = ref(uni.getStorageSync('token'))
onShow(()=>{
token.value = uni.getStorageSync('token')
})
</script>
<style lang="scss">
.noToken{
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
z-index: 2;
pointer-events: auto;
}
</style>

336
components/siteDetail.vue Normal file
View File

@ -0,0 +1,336 @@
<template>
<view class="site-detail">
<!-- 店铺图片 -->
<image @click="shopImgClick(siteItem)" :src="baseImageUrl + siteItem.imgUrl" class="shop-image" :class="siteItem.isFiveGoatStores?'shop-imageBig':''"></image>
<view class="shop-introduce">
<view class="shop-introduce-title">
<text>{{ siteItem.name }}</text>
<text class="navi-text" @click="handleNavigate">
<text v-if="siteItem.distance">{{ siteItem.distance }}KM</text>
[ {{ $t("home.navigate") }} ]
</text>
</view>
<view class="shop-region">
<view class="left-wrap">
<view class="text-wrap">
<uv-icon name="landmark" custom-prefix="custom-icon" size="12" color="#616e78"></uv-icon>
<text class="text">{{ siteItem.city }}</text>
</view>
<view class="text-wrap">
<uv-icon name="bus" custom-prefix="custom-icon" size="12" color="#616e78"></uv-icon>
<text class="text">
{{ siteItem.district }}
<text style="color: red;font-weight: bold;"
@click="handleWayfinding(2)"
v-if="siteItem.isStopCarGuide">
[ P ]
</text>
</text>
</view>
</view>
<text class="navi-text" @click="handleWayfinding(1)">[ {{ $t("home.wayfinding") }} ]</text>
</view>
<view class="shop-address">
<textEllipsis style="width: 100%;" :address="siteItem.address"></textEllipsis>
</view>
<button @click="handleBook(siteItem.id)" class="shop-booknow">
<!-- #ifdef MP-WEIXIN || H5 || APP-PLUS -->
{{ $t("home.book") }}
<!-- #endif -->
<!-- #ifdef MP-XHS -->
{{ $t("home.quote") }}
<!-- #endif -->
</button>
<view @click="handleSite" v-if="siteItem.isFiveGoatStores" class="shop-booknow isFiveGoatStoresBtn">
{{ state.isShow ?$t("common.Collapse"):$t("common.Expand") }} 五羊门店 {{ $t("common.OtherStores") }} <view class="isFiveGoatStoresBtn-icon" :class="state.isShow?'rotate':''"><uv-icon name="arrow-down-fill" color="black" size="16"></uv-icon></view>
</view>
</view>
</view>
<view class="site-detail isFiveGoatStoresSiteDetail" v-if="state.isShow" v-for="item in siteItem.siteList" :key="item.id">
<image @click="shopImgClick(item)" :src="baseImageUrl + item.imgUrl" class="shop-image"></image>
<view class="shop-introduce">
<view class="shop-introduce-title">
<text>{{ item.name }}</text>
</view>
<button @click="handleBook(item.id)" class="shop-booknow">
<!-- #ifdef MP-WEIXIN || H5 || APP-PLUS -->
{{ $t("home.book") }}
<!-- #endif -->
<!-- #ifdef MP-XHS -->
{{ $t("home.quote") }}
<!-- #endif -->
</button>
</view>
</view>
<view v-if="state.isShow" class="closeList" @click="handleSite">
{{ $t("common.Collapse") }} 五羊店 <uv-icon name="arrow-up-fill" color="black" size="20"></uv-icon>
</view>
</template>
<script setup>
import { baseImageUrl, isH5, isXiaohongshu, AppId,envVersion } from "@/config/index.js";
import { h5GoWx, jumpToSc } from "@/utils/common.js";
import { useMainStore } from "@/store/index.js";
import { ref } from "vue";
const { storeState } = useMainStore();
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import textEllipsis from "@/components/textEllipsis.vue";
const props = defineProps({
siteItem: {
type: Object,
default: () => { },
},
});
const state = ref({
isShow:false,
})
const emit = defineEmits(["showCode"]);
const handleSite = (e)=>{
state.value.isShow = !state.value.isShow
}
// 线
const handleNavigate = () => {
uni.openLocation({
latitude: Number(props.siteItem.latitude),
longitude: Number(props.siteItem.longitude),
name: props.siteItem.name,
address: props.siteItem.address,
});
};
//
const handleWayfinding = (type) => {
uni.navigateTo({
url: `/pages/book/navigate?id=${props.siteItem.id}&type=${type}`,
});
};
const shopImgClick = (item) => {
//
if (storeState.userInfo?.userType == 2 || storeState.userInfo?.userType == 3) {
// if (props.siteItem.accesscontrol == null || props.siteItem.accesscontrol == '') {
// uni.showToast({
// title: t('site.noAccessId'),
// icon: 'none'
// });
// } else {
// //
// if (storeState.userInfo?.siteList?.includes('*') || storeState.userInfo?.siteList?.includes(props.siteItem.id)) {
// emit("showCode", props.siteItem);
// } else {
// uni.showToast({
// title: t('site.noPermission'),
// icon: 'none'
// });
// }
// }
//
if (storeState.userInfo?.siteList?.includes('*') || storeState.userInfo?.siteList?.includes(item.id)) {
emit("showCode", item);
} else {
uni.showToast({
title: t('site.noPermission'),
icon: 'none'
});
}
return;
}
if (item.vrUrl) {
uni.navigateTo({
url: "/pages/webview/web?url=" + encodeURIComponent(item.vrUrl),
});
} else {
handleBook(item.id);
}
};
const handleBook = (id) => {
// H5
if (isH5 && !isXiaohongshu) {
h5GoWx()
return;
}
// ID
let targetItem = jumpToSc.find((item) => item.id == id);
if (targetItem) {
uni.navigateToMiniProgram({
appId: "wx3002eda9d707a977",
path: `/pages/site/index?id=${targetItem.targetId}`,
});
return;
}
//
if(AppId=="wxb20921dfdd0b94f4" && envVersion === 'release'){
uni.navigateToMiniProgram({
appId: "wx3c4ab696101d77d1",
path: `/pages/site/index?id=${id}`,
});
return;
}
uni.navigateTo({
url: `/pages/site/index?id=${id}&name=${props.siteItem.name}`,
});
};
</script>
<style lang="scss">
.site-detail {
width: 750rpx;
min-height: 258rpx;
box-shadow: 0rpx 0rpx 10rpx 0rpx rgba(0, 0, 0, 0.14);
background-color: #fff;
margin-top: 4rpx;
padding: 24rpx 0;
display: flex;
align-items: center;
&>.shop-image {
width: 200rpx;
height: 200rpx;
border-radius: 16rpx;
margin: 0 30rpx;
}
&>.shop-imageBig{
height: 250rpx;
}
&>.shop-introduce {
width: 460rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
&>.shop-introduce-title {
width: 100%;
height: 54rpx;
display: flex;
justify-content: flex-start;
align-items: center;
&>text {
font-weight: bold;
font-size: 32rpx;
line-height: 36rpx;
}
&>.shop-introduce-hot {
padding: 6rpx 12rpx;
border-radius: 4rpx;
margin-left: 10rpx;
background-color: #ff0000;
text-align: center;
font-size: 16rpx;
font-weight: bold;
letter-spacing: 1px;
color: #ffffff;
}
.navi-text {
color: #616e78;
opacity: 0.8;
font-size: var(--f22);
margin-left: auto;
}
}
&>.shop-region {
width: 100%;
flex-wrap: wrap;
display: flex;
justify-content: flex-start;
align-items: center;
font-size: var(--f21);
margin: 10rpx 0;
.left-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
flex: 1;
.text-wrap {
display: flex;
.text {
margin: 0 8rpx;
color: #616e78;
font-size: var(--f22);
font-weight: bold;
line-height: 1.4;
}
}
}
.navi-text {
font-size: var(--f22);
font-weight: bold;
color: red;
}
}
&>.shop-address {
width: 100%;
flex-wrap: wrap;
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--f21);
}
&>.shop-booknow {
width: 100%;
height: 60rpx;
line-height: 60rpx;
background-color: var(--main-color);
color: var(--text-color);
font-size: 26rpx;
font-weight: 700;
}
&>.isFiveGoatStoresBtn{
display: flex;
justify-content: center;
background-color: var(--left-linear2);
margin-top: 20rpx;
text-align: center;
border-radius: 8rpx;
.isFiveGoatStoresBtn-icon{
display: flex;
align-items: center;
}
.rotate{
transform: rotate(180deg);
}
}
}
}
.isFiveGoatStoresSiteDetail{
min-height: 150rpx;
margin: 10rpx 0;
// align-items: flex-start;
.shop-image {
width: 200rpx;
height: 160rpx;
object-fit: contain;
}
.shop-introduce-title{
text {
margin-bottom: 40rpx;
}
}
}
.closeList{
display: flex;
justify-content: center;
padding: 10rpx 0;
}
</style>

110
components/textEllipsis.vue Normal file
View File

@ -0,0 +1,110 @@
<template>
<view class="text-wrap" @longpress="handleLongTap">
<uv-icon name="map1" custom-prefix="custom-icon" size="12" color="#616e78"></uv-icon>
<view class="inner-text" ref="innerText" :class="{ over: state.over, show: state.showDropdown }" @click.stop.prevent="handleClick">
<view class="arrow">
<uv-icon :name="state.over ? 'arrow-down' : 'arrow-up'" size="8" color="#616e78"></uv-icon>
</view>
<view class="text">{{ props.address }}</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, onMounted } from "vue";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const instance = getCurrentInstance();
const props = defineProps(["address"]);
const innerText = ref(null);
const state = reactive({
over: false,
showDropdown: false
});
onMounted(() => {
// #ifdef MP-WEIXIN
const query = uni.createSelectorQuery().in(instance.proxy);
query
.select(".inner-text")
.boundingClientRect((data) => {
let height = data.height;
if (height > 18) {
state.over = true;
state.showDropdown = true;
}
})
.exec();
// #endif
// #ifndef MP-WEIXIN
let height = innerText.value?.$el?.offsetHeight;
if (height > 18) {
state.over = true;
state.showDropdown = true;
}
// #endif
});
const handleClick = () => {
if (!state.showDropdown) return;
state.over = !state.over;
}
const handleLongTap = () => {
uni.setClipboardData({
data: props.address,
showToast: false,
success: function () {
uni.showToast({
title: t("toast.copy"),
icon: "none"
});
}
});
}
</script>
<style lang="scss">
.text-wrap {
display: flex;
align-items: baseline;
width: 100%;
margin-bottom: 10rpx;
.inner-text {
position: relative;
width: calc(100% - 20rpx);
.arrow {
display: none;
position: absolute;
right: -20rpx;
bottom: 0;
}
.text {
margin-left: 4px;
color: #616e78;
font-size: var(--f22);
font-weight: bold;
}
}
.over {
.text {
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.show {
.arrow {
display: block;
}
}
}
</style>

212
components/updatePopup.vue Normal file
View File

@ -0,0 +1,212 @@
<template>
<uv-popup
ref="popup"
class="update-popup"
:class="[`${themeInfo.theme}-theme`]"
custom-style="width: 90%; border-radius: 16rpx;"
>
<view class="update-wrap">
<view class="auth-btn" @click="wxGetUserProfile">{{ $t("login.wxLogin") }}</view>
</view>
</uv-popup>
<uv-popup
ref="phonePopup"
customStyle="width: 80%; height: 400rpx; padding:20rpx 0; border-radius: 32rpx; display: flex; flex-direction: column; justify-content: center; align-items: center;"
@change="popupChange"
>
<text>{{ $t("common.bindPhone") }}</text>
<text style="padding: 0 40rpx; margin-top: 20rpx; text-align: center;"> {{ $t("common.bindPhoneAfter") }}</text>
<button
style="width: 80%; margin-top: 20px; background: #5BBC6B; color: #FFFFFF; line-height: 80rpx;"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
{{ $t("common.QuickBind") }}
</button>
</uv-popup>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useMainStore } from "@/store/index.js";
import { projectInfo } from "@/config/index.js";
const { themeInfo, getUserInfo, storeState } = useMainStore();
import { useLoginApi } from "@/Apis/login.js";
const getApi = useLoginApi();
const popup = ref(null);
const phonePopup = ref(null);
const props = defineProps({
name: {
type: String,
default: "",
},
});
const open = () => {
popup.value.open();
};
defineExpose({ open });
const code = ref("");
const encryptedData = ref("");
const iv = ref("");
const wxLogin = () => {
// code
return new Promise(function (reslove, reject) {
wx.login({
success(res) {
code.value = res.code;
reslove(res.code);
},
fail: (err) => {
reject(err);
console.error("wx.login调用失败", err);
},
});
});
};
const AuthorizedLogin = (data) => {
uni.showLoading();
getApi.AuthorizedLogin(data).then(async (res) => {
uni.hideLoading();
if (res.code == 200) {
storeState.token = res.data.token;
uni.setStorageSync("token", res.data.token);
uni.setStorageSync("openId", res.data.openId);
uni.removeStorage({
key:'Pre_ID'
})
uni.removeStorage({
key:'mediatorId'
})
const { data: userInfo } = await getUserInfo();
if (!userInfo.phone) {
phonePopup.value.open();
}
popup.value.close();
}
});
};
const wxGetUserProfile = async () => {
uni.showLoading();
wx.getUserProfile({
desc: "用于完善会员资料", //
success: async (res) => {
encryptedData.value = res.encryptedData;
iv.value = res.iv;
// code
uni.checkSession({
success: (res) => {
//
AuthorizedLogin({
code: code.value,
encryptedData: encryptedData.value,
iv: iv.value,
openId: uni.getStorageSync("openId"),
Pre_ID: uni.getStorageSync("Pre_ID"),
mediatorId: uni.getStorageSync('mediatorId')
});
},
fail: (err) => {
wxLogin().then((res) => {
AuthorizedLogin({
code: code.value,
encryptedData: encryptedData.value,
iv: iv.value,
openId: uni.getStorageSync("openId"),
Pre_ID: uni.getStorageSync("Pre_ID"),
mediatorId: uni.getStorageSync('mediatorId')
});
});
},
});
},
fail: (err) => {
uni.hideLoading();
console.error("获取用户信息失败:", err);
},
});
}
const getPhoneNumber = async (e) => {
uni.showLoading();
if (e.detail.code) {
getApi.GetPhoneNumber({code:e.detail.code}).then((res) => {
if (res.code == 200) {
uni.hideLoading();
phonePopup.value.close();
}
});
} else {
uni.hideLoading();
uni.showToast({
title: "获取手机号失败",
icon: "none",
duration: 2000,
});
}
}
onLoad(() => {
if (uni.getAppBaseInfo().hostName === "WeChat") {
wxLogin();
}
});
</script>
<style lang="scss" scoped>
.update-wrap {
overflow: hidden;
background: #ffffff;
.title-wrap {
display: flex;
align-items: center;
justify-content: center;
height: 120rpx;
background: #f7f7f7;
.title {
display: flex;
position: relative;
color: #242e42;
z-index: 2;
&::after {
content: "";
position: absolute;
bottom: 2px;
left: -10px;
width: calc(100% + 20px);
height: 6px;
border-radius: 4px;
background: var(--main-color);
z-index: -1;
}
}
}
.content {
padding: 50rpx 40rpx 10rpx;
.text {
color: #292927;
margin-bottom: 40rpx;
}
}
.auth-btn {
margin: 0 40rpx 60rpx;
padding: 20rpx 0;
text-align: center;
border-radius: 10rpx;
letter-spacing: 2px;
background: var(--main-color);
}
}
</style>

View File

@ -0,0 +1,71 @@
export default {
props: {
// 滑块的移动过渡时间单位ms
duration: {
type: Number,
default: 300
},
// tabs标签数组
list: {
type: Array,
default: () => []
},
// 滑块颜色
lineColor: {
type: String,
default: '#3c9cff'
},
// 菜单选择中时的样式
activeStyle: {
type: [String, Object],
default: () => ({
color: '#303133'
})
},
// 菜单非选中时的样式
inactiveStyle: {
type: [String, Object],
default: () => ({
color: '#606266'
})
},
// 滑块长度
lineWidth: {
type: [String, Number],
default: 20
},
// 滑块高度
lineHeight: {
type: [String, Number],
default: 3
},
// 滑块背景显示大小,当滑块背景设置为图片时使用
lineBgSize: {
type: String,
default: 'cover'
},
// 菜单item的样式
itemStyle: {
type: [String, Object],
default: () => ({
height: '44px'
})
},
// 菜单是否可滚动
scrollable: {
type: Boolean,
default: true
},
// 当前选中标签的索引
current: {
type: [Number, String],
default: 0
},
// 默认读取的键名
keyName: {
type: String,
default: 'name'
},
...uni.$uv?.props?.tabs
}
}

View File

@ -0,0 +1,412 @@
<template>
<view class="uv-tabs" :style="[$uv.addStyle(customStyle)]">
<view class="uv-tabs__wrapper">
<slot name="left" />
<view class="uv-tabs__wrapper__scroll-view-wrapper">
<scroll-view
:scroll-x="scrollable"
:scroll-left="scrollLeft"
scroll-with-animation
class="uv-tabs__wrapper__scroll-view"
:show-scrollbar="false"
ref="uv-tabs__wrapper__scroll-view"
>
<view
class="uv-tabs__wrapper__nav"
ref="uv-tabs__wrapper__nav"
:style="{
flex: scrollable ? '' : 1
}"
>
<view
class="uv-tabs__wrapper__nav__item"
v-for="(item, index) in list"
:key="index"
@tap="clickHandler(item, index)"
:ref="`uv-tabs__wrapper__nav__item-${index}`"
:style="[{flex: scrollable ? '' : 1},$uv.addStyle(itemStyle)]"
:class="[`uv-tabs__wrapper__nav__item-${index}`, item.disabled && 'uv-tabs__wrapper__nav__item--disabled']"
>
<!-- <text
:class="[item.disabled && 'uv-tabs__wrapper__nav__item__text--disabled']"
class="uv-tabs__wrapper__nav__item__text"
:style="[textStyle(index)]"
>{{ item[keyName] }}</text> -->
<view class="slfeTabs">
<view class="name">{{ item[keyName] }}</view>
<view :style="[textStyle(index)]" class="line"></view>
<view class="areaRange">{{ item['areaRange'] }}</view>
</view>
<uv-badge
:show="!!(item.badge && (item.badge.show || item.badge.isDot || item.badge.value))"
:isDot="item.badge && item.badge.isDot || propsBadge.isDot"
:value="item.badge && item.badge.value || propsBadge.value"
:max="item.badge && item.badge.max || propsBadge.max"
:type="item.badge && item.badge.type || propsBadge.type"
:showZero="item.badge && item.badge.showZero || propsBadge.showZero"
:bgColor="item.badge && item.badge.bgColor || propsBadge.bgColor"
:color="item.badge && item.badge.color || propsBadge.color"
:shape="item.badge && item.badge.shape || propsBadge.shape"
:numberType="item.badge && item.badge.numberType || propsBadge.numberType"
:inverted="item.badge && item.badge.inverted || propsBadge.inverted"
customStyle="margin-left: 4px;"
></uv-badge>
</view>
<!-- #ifdef APP-NVUE -->
<view
class="uv-tabs__wrapper__nav__line"
ref="uv-tabs__wrapper__nav__line"
:style="[{
width: $uv.addUnit(lineWidth),
height: firstTime?0:$uv.addUnit(lineHeight),
background: lineColor,
backgroundSize: lineBgSize
}]"
>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<view
class="uv-tabs__wrapper__nav__line"
ref="uv-tabs__wrapper__nav__line"
:style="[{
width: $uv.addUnit(lineWidth),
transform: `translate(${lineOffsetLeft}px)`,
transitionDuration: `${firstTime ? 0 : duration}ms`,
height: firstTime?0:$uv.addUnit(lineHeight),
background: lineColor,
backgroundSize: lineBgSize,
}]"
>
<!-- #endif -->
</view>
</view>
</scroll-view>
</view>
<slot name="right" />
</view>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import uvBadgeProps from '@/uni_modules/uv-badge/components/uv-badge/props.js'
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation')
const dom = uni.requireNativePlugin('dom')
// #endif
import props from './props.js';
/**
* Tabs 标签
* @description tabs标签组件在标签多的时候可以配置为左右滑动标签少的时候可以禁止滑动 该组件的一个特点是配置为滚动模式时激活的tab会自动移动到组件的中间位置
* @tutorial https://www.uvui.cn/components/tabs.html
* @property {Array} list 标签数组元素为对象[{name: '推荐'}]
* @property {String | Number} duration 滑块移动一次所需的时间单位秒默认 200
* @property {String | Object} activeStyle 菜单选择中时的样式默认{ color: '#303133' }
* @property {String | Object} inactiveStyle 菜单非选择中时的样式默认{ color: '#606266' }
* @property {String | Number} lineWidth 滑块长度默认 20
* @property {String | Number} lineHeight 滑块高度默认 3
* @property {String} lineColor 滑块颜色默认'#3c9cff'
* @property {String} lineBgSize 滑块背景显示大小当滑块背景设置为图片时使用默认 cover
* @property {String | Number} itemStyle 菜单item的样式默认 { height: '44px' }
* @property {String} scrollable 菜单是否可滚动选项很少的时候设置为false整个tabs自动居中显示默认true
* @property {String | Number} current 当前选中标签的索引默认 0
* @property {String} keyName 从list元素对象中读取的键名默认 'name'
* @property {String | Number} swierWidth swiper的宽度默认 '750rpx'
* @property {String | Object} customStyle 自定义外部样式
*
* @event {Function(index)} change 标签改变时触发 index: 点击了第几个tab索引从0开始
* @event {Function(index)} click 点击标签时触发 index: 点击了第几个tab索引从0开始
* @example <uv-tabs :list="list" :is-scroll="false" :current="current" @change="change"></uv-tabs>
*/
export default {
name: 'uv-tabsSelf',
emits: ['click','change'],
mixins: [mpMixin, mixin, props],
data() {
return {
firstTime: true,
scrollLeft: 0,
scrollViewWidth: 0,
lineOffsetLeft: 0,
tabsRect: {
left: 0
},
innerCurrent: 0,
moving: false,
}
},
watch: {
current: {
immediate: true,
handler (newValue, oldValue) {
//
if (newValue !== this.innerCurrent) {
this.innerCurrent = newValue
this.$nextTick(() => {
this.resize()
})
}
}
},
// listlist
list() {
this.$nextTick(() => {
this.resize()
})
}
},
computed: {
textStyle() {
return index => {
const style = {}
//
const customeStyle = index == this.innerCurrent ? this.$uv.addStyle(this.activeStyle) : this.$uv
.addStyle(
this.inactiveStyle)
// nvuestyle!import
if (this.list[index].disabled) {
style.color = '#c8c9cc'
}
return this.$uv.deepMerge(customeStyle, style)
}
},
propsBadge() {
return uvBadgeProps
}
},
async mounted() {
this.init()
},
methods: {
setLineLeft() {
const tabItem = this.list[this.innerCurrent];
if (!tabItem) {
return;
}
//
let lineOffsetLeft = this.list
.slice(0, this.innerCurrent)
.reduce((total, curr) => total + curr.rect.width, 0);
// 线px
let lineWidth = this.$uv.getPx(this.lineWidth);
// +
if (this.$uv.test.number(this.lineWidth) && this.$uv.unit) {
lineWidth = this.$uv.getPx(`${this.lineWidth}${this.$uv.unit}`);
}
this.lineOffsetLeft = lineOffsetLeft + (tabItem.rect.width - lineWidth) / 2
// #ifdef APP-NVUE
//
this.animation(this.lineOffsetLeft, this.firstTime ? 0 : parseInt(this.duration))
// #endif
// tab item
// nvuestylefalse()
if (this.firstTime) {
setTimeout(() => {
this.firstTime = false
}, 20);
}
},
// nvue
animation(x, duration = 0) {
// #ifdef APP-NVUE
const ref = this.$refs['uv-tabs__wrapper__nav__line']
animation.transition(ref, {
styles: {
transform: `translateX(${x}px)`
},
duration
})
// #endif
},
//
clickHandler(item, index) {
// disabledclickchange
this.$emit('click', {
...item,
index
})
// disabled
if (item.disabled) return
if(this.innerCurrent != index) {
this.$emit('change', {
...item,
index
})
}
this.innerCurrent = index
// #ifndef APP-NVUE
this.$nextTick(()=>{
this.resize()
})
// #endif
// #ifdef APP-NVUE
this.$nextTick(()=>{
// nvue
this.$uv.sleep(30).then(res=>{
this.resize()
});
})
// #endif
},
init() {
this.$uv.sleep().then(() => {
this.resize()
})
},
setScrollLeft() {
// tabtabwidthleft()
const tabRect = this.list[this.innerCurrent]
// item
const offsetLeft = this.list
.slice(0, this.innerCurrent)
.reduce((total, curr) => {
return total + curr.rect.width
}, 0)
//
const windowWidth = this.$uv.sys().windowWidth
// tabs-itemscroll-view
let scrollLeft = offsetLeft - (this.tabsRect.width - tabRect.rect.width) / 2 - (windowWidth - this.tabsRect
.right) / 2 + this.tabsRect.left / 2
// scrollLeftscroll-viewtabs
scrollLeft = Math.min(scrollLeft, this.scrollViewWidth - this.tabsRect.width)
this.scrollLeft = Math.max(0, scrollLeft)
},
//
resize() {
// list
if(this.list.length === 0) {
return
}
Promise.all([this.getTabsRect(), this.getAllItemRect()]).then(([tabsRect, itemRect = []]) => {
this.tabsRect = tabsRect
this.scrollViewWidth = 0
itemRect.map((item, index) => {
// scroll-view
this.scrollViewWidth += item.width
// itemX
this.list[index].rect = item
})
// tabs
this.setLineLeft()
this.setScrollLeft()
})
},
//
getTabsRect() {
return new Promise(resolve => {
this.queryRect('uv-tabs__wrapper__scroll-view').then(size => resolve(size))
})
},
//
getAllItemRect() {
return new Promise(resolve => {
const promiseAllArr = this.list.map((item, index) => this.queryRect(
`uv-tabs__wrapper__nav__item-${index}`, true))
Promise.all(promiseAllArr).then(sizes => resolve(sizes))
})
},
//
queryRect(el, item) {
// #ifndef APP-NVUE
// $uvGetRectuni-uihttps://www.uvui.cn/js/getRect.html
// this.$uvGetRectgetRect
return new Promise(resolve => {
this.$uvGetRect(`.${el}`).then(size => {
resolve(size)
})
})
// #endif
// #ifdef APP-NVUE
// nvue使dom
// promise使then
return new Promise(resolve => {
dom.getComponentRect(item ? this.$refs[el][0] : this.$refs[el], res => {
resolve(res.size)
})
})
// #endif
},
},
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
.uv-tabs {
.slfeTabs{
margin: 16rpx 8rpx;
view{
text-align: center;
font-size: 24rpx;
letter-spacing: 2rpx;
}
.line{
width: 100%;
height: 2px;
margin: 10rpx 0;
border-radius: 4px;
background: #333;
}
}
&__wrapper {
@include flex;
align-items: center;
&__scroll-view-wrapper {
flex: 1;
/* #ifndef APP-NVUE */
overflow: auto hidden;
/* #endif */
}
&__scroll-view {
@include flex;
flex: 1;
}
&__nav {
@include flex;
position: relative;
&__item {
padding: 0 11px;
@include flex;
align-items: center;
justify-content: center;
&--disabled {
/* #ifndef APP-NVUE */
cursor: not-allowed;
/* #endif */
}
&__text {
font-size: 15px;
color: $uv-content-color;
&--disabled {
color: $uv-disabled-color !important;
}
}
}
&__line {
height: 3px;
background: $uv-primary;
width: 30px;
position: absolute;
bottom: 2px;
border-radius: 100px;
transition-property: transform;
transition-duration: 300ms;
}
}
}
}
</style>

51
components/wxNavbar.vue Normal file
View File

@ -0,0 +1,51 @@
<template>
<!-- #ifdef MP-WEIXIN || MP-XHS -->
<view class="wxNavbar" :style="{ height: `${state.navHeight}px` }">
<view v-if="state.navHeight" class="title" :style="{ 'margin-top': `${state.statusBarHeight}px` }">{{ props.title }}</view>
</view>
<!-- #endif -->
</template>
<script setup>
// #ifdef MP-WEIXIN || MP-XHS
import { reactive, onMounted } from "vue";
import { navbarHeightAndStatusBarHeight } from "@/utils/common.js";
const props = defineProps({
title: {
type: String,
default: "EliteSys"
}
});
let state = reactive({
navHeight: 0,
statusBarHeight: 0,
});
onMounted(() => {
const heightData = navbarHeightAndStatusBarHeight();
state.navHeight = heightData.navbarHeight;
state.statusBarHeight = heightData.statusBarHeight;
});
// #endif
</script>
<style lang="scss" scoped>
.wxNavbar {
z-index: 999999;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
background: linear-gradient(to left, var(--left-linear), var(--right-linear));
&::after {
content: "";
position: absolute;
width: 100%;
height: 2px;
bottom: -2px;
background: linear-gradient(to left, var(--left-linear), var(--right-linear));
}
}
</style>

61
config/index.js Normal file
View File

@ -0,0 +1,61 @@
export const isKingKong = true;
export const isShichang = !isKingKong; // 是否是时昌小程序环境
const systemInfo = uni.getSystemInfoSync();
export const isWeChatMiniProgram = systemInfo.hostName === 'WeChat'; // 是否是微信小程序环境
export const isXiaohongshu = systemInfo.hostName === 'xhs'; // 是否是小红书环境
const isDev = process.env.NODE_ENV === 'development' // 是否是开发环境
export const OFFICIAL_URL = 'https://elitesys.kingkongcang.com/Mini' // 跳转小程序地址
// 如果是小程序 就要写全域名, 如果是h5 自动获取域名 兼容测试环境(http://8.134.73.118:10000/api 不用进行跨域操作) 跟正式环境 自动获取域名补充
export const devURL = "https://uat.kingkongcang.com/adminApi" // 开发环境地址 'http://118.145.200.78:10000/api http://localhost:5182/api' "https://dev.kingkongcang.com/adminApi"
export const testURL = isKingKong?"https://uat.kingkongcang.com/adminApi":"https://www.scstorage.net/adminApi" // 测试环境地址
export const prodURL = isKingKong?"https://elitesys.kingkongcang.com/adminApi":"https://www.scstorage.net/adminApi" // 正式环境地址
export let IsApp = false
// #ifdef APP-PLUS
IsApp = true
// #endif
export let isH5 = false// h5环境下 下单会跳转到小程序
export let RElEASE_DATE = '2026/02/26'
let accountInfo = {}
// #ifdef MP-WEIXIN || MP-XHS
accountInfo = uni.getAccountInfoSync();
isH5 = false
// #endif
export const AppId = accountInfo?.miniProgram?.appId
export const envVersion = accountInfo?.miniProgram?.envVersion
const returnBaseUrl = () => {
if (isWeChatMiniProgram || isXiaohongshu || IsApp) {
if (envVersion === 'develop') {
return devURL;
} else if (envVersion === 'trial') {
return testURL;
} else if (envVersion === 'release') {
return prodURL;
} else {
return prodURL;
}
// 预留 防止出错
return isDev ? testURL: prodURL; // https://elitesys.kingkongcang.com/adminApi // 测试环境 https://uat.kingkongcang.com/adminApi 开发环境 https://dev.kingkongcang.com/adminApi http://localhost:5182/
} else {
return isDev
? `${window.location.origin}/api`
: `${window.location.origin}/adminApi`;
}
};
export const baseUrl = returnBaseUrl();
export const baseImageUrl = isKingKong? 'https://elitesoss.oss-cn-guangzhou.aliyuncs.com/':'https://scstorage.oss-cn-guangzhou.aliyuncs.com/'
export const currency = isKingKong ? '¥' : '¥';
// 小程序默认金刚配色先
export const theme = ((isWeChatMiniProgram || isXiaohongshu) && isKingKong) ? 'golden' : 'default'; // 默认主题 - "default" 金刚色主题 - "golden"
export const projectInfo = {
name: isKingKong ? '金刚迷你仓' : '时昌迷你仓', // 名字:金刚迷你仓、时昌迷你仓
miniName: isKingKong ? '金刚迷你仓' : '时昌迷你仓', // 小程序名字:迷你仓订仓、时昌迷你仓
phone: isKingKong ? '400-818-1813' : '15323894878',
callPhone: isKingKong ? '4008181813' : '15323894878'
};
export const watermarkURL= '?x-oss-process=image/watermark,text_5Zu-54mH6K6k6K-B5LiT55So,t_80,g_center,rotate_45,color_FF0000,size_100'
// 时昌微信二维码
export const scWechatImg = baseImageUrl + "d3572937-4a9c-410e-992b-c19ff03e9ada.jpg"
export const setOrderDays = isKingKong ? 30 : 7; // 金刚小程序默认7天其他默认30天

43
hooks/index.js Normal file
View File

@ -0,0 +1,43 @@
import { ref,onBeforeMount} from "vue";
/**
* 倒计时
* @param {Number} second 倒计时秒数
* @return {Number} count 倒计时秒数
* @return {Function} countDown 倒计时函数
* @example
* const { count, countDown } = useCountDown()
* countDown(60)
* <div>{{ count }}</div>
*/
export function useCountDown() {
const count = ref(0)
const timer = ref(null);
const countDown = (second = 60, ck = () => { }) => {
if (count.value === 0 && timer.value === null) {
ck();
count.value = second;
timer.value = setInterval(() => {
count.value--
if (count.value === 0) {
clearInterval(timer.value)
timer.value = null
}
}, 1000);
}
};
const cancelCout=()=>{
clearInterval(timer.value)
timer.value = null
count.value = 0
}
onBeforeMount(() => {
timer.value && clearInterval(timer.value)
});
return {
count,
countDown,
cancelCout
};
}

68
hooks/useCountDown.js Normal file
View File

@ -0,0 +1,68 @@
import { ref, onUnmounted } from 'vue'
import dayjs from 'dayjs'
export function useCountDown(startTime, endTime, onFinished) {
if (!startTime || !endTime) {
throw new Error('startTime and endTime are required')
}
const remaining = ref(0)
const formatted = ref('00:00:00')
let timer = null
const toTimestamp = (t) => dayjs(t).valueOf()
let startTs = toTimestamp(startTime)
let endTs = toTimestamp(endTime)
const calc = () => {
const now = dayjs().valueOf()
if (now < startTs) {
remaining.value = startTs - now
} else if (now >= startTs && now < endTs) {
remaining.value = endTs - now
} else {
remaining.value = 0
clearInterval(timer)
timer = null
onFinished && onFinished()
}
format()
}
const format = () => {
let left = remaining.value
let totalSeconds = Math.floor(left / 1000)
const days = Math.floor(totalSeconds / 86400)
totalSeconds %= 86400
const h = String(Math.floor(totalSeconds / 3600)).padStart(2, '0')
const m = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0')
const s = String(totalSeconds % 60).padStart(2, '0')
formatted.value = days > 0 ? `${days}${h}:${m}:${s}` : `${h}:${m}:${s}`
}
const start = () => {
// 如果已有计时器,先清掉
if (timer) clearInterval(timer)
startTs = toTimestamp(startTime)
endTs = toTimestamp(endTime)
calc()
timer = setInterval(calc, 1000)
}
const reset = (newStart, newEnd) => {
startTime = newStart
endTime = newEnd
start()
}
onUnmounted(() => {
if (timer) clearInterval(timer)
})
return {
formatted,
remaining,
start,
reset
}
}

54
hooks/useLocation.js Normal file
View File

@ -0,0 +1,54 @@
import { reactive } from "vue";
import { useMainStore } from "@/store/index";
export function useLocation() {
const { storeState, setLocation } = useMainStore();
const locationState = reactive({
showGetLocation: false,
latitude: 0,
longitude: 0
});
const getLocation = () => {
return new Promise((resolve) => {
// 1⃣ 优先使用 store 里的定位
if (storeState.location?.latitude && storeState.location?.longitude) {
locationState.latitude = storeState.location.latitude;
locationState.longitude = storeState.location.longitude;
resolve(true);
return;
}
// 2⃣ 本地已经有定位
if (locationState.latitude && locationState.longitude) {
resolve(true);
return;
}
// 3⃣ 没有定位才请求
SFUIP.getLocation().then(res => {
if (!res.success) {
resolve(false);
return;
}
const { latitude, longitude } = res.data || {};
if (latitude && longitude) {
locationState.latitude = latitude;
locationState.longitude = longitude;
locationState.showGetLocation = false;
setLocation({ latitude, longitude });
resolve(true);
} else {
resolve(false);
}
});
});
};
return { locationState, getLocation };
}

50
index.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> -->
<link rel="icon" href="/favicon.ico" />
<script src="https://ucmp-static.sf-express.com/assets/sdks/microservice-1.0.4.min.js"></script>
<script>
const sf = new SFUIP.SfMicroservice('prod')
window.sf = sf
</script>
<script>
// // 获取当前路径名
// const currentPath = window.location.pathname;
// var currentDomain = window.location.origin;
// // 定义跳转逻辑
// function checkAndRedirect() {
// const screenWidth =document.documentElement.clientWidth;
// if (screenWidth >= 900 && !(currentPath.indexOf('/h5/index.html') ==-1)) {
// window.location.href =currentDomain+ '/';
// } else if (screenWidth < 900 && (currentPath.indexOf('/h5/index.html') ==-1)) {
// window.location.href =currentDomain+ '/h5/index.html';
// }
// }
// // // 初始检查
// checkAndRedirect();
// // 监听屏幕宽度变化
// window.addEventListener('resize', () => {
// checkAndRedirect();
// });
</script>
<title>金刚迷你仓</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

502
locale/en.json Normal file
View File

@ -0,0 +1,502 @@
{
"common.confirm": "Confirm",
"common.cancel": "Cancel",
"common.title": "Tips",
"common.noData": "No Data",
"common.delete": "Delete",
"common.update": "Update",
"common.password": "password",
"common.close": "Close",
"common.Skiptoday": "Skip today",
"common.payableTime": "Payable Time",
"common.AuthorizationOrder": "Authorization Order",
"common.cancelOrder": "Cancel Order",
"common.cancelOrderTips": "Are you sure you want to cancel the current order?",
"common.unpaidOrderTips": "There are unpaid orders, please pay first, or cancel the order before placing a new one!",
"common.cantUselocker": "This locker cannot be rented, please re-select!",
"common.userName": "User Name",
"common.logout": "Log Out",
"common.logoutTip": "Confirm logout?",
"common.FirstTimeLoginTips": "First-time login will automatically create an account.",
"common.more": "More",
"common.SpaceSpecsGuide": "Space Specs Guide",
"common.Seelegend": "See legend",
"common.Rent": "Rent",
"common.OriginalPrice": "Original Price",
"common.SalePrice": "Sale Price",
"common.SwitchStores": "Switch Stores",
"common.SwitchRegion": "Switch Region",
"common.Countdown": "Countdown",
"common.FlashSale": "Flash Sale",
"common.FlashSalePrice": "Flash Sale Price",
"common.ClickToCheck": "Click To Check",
"common.ClickToZoomIn": "Click To Zoom In",
"common.tryZooming": "If access recognition fails, try clicking to zoom in",
"common.OnSiteAssessment": "On-Site Assessment",
"common.ConsultationQuotation": "Consultation & quotation",
"common.Requirement": "Requirement",
"common.day": "day(s)",
"common.reset": "Reset",
"common.notRented": "Not rented",
"common.rented": "Rented",
"common.locked": "Locked",
"locale.auto": "System",
"locale.en": "English",
"locale.zh-hans": "简体中文",
"locale.zh-hant": "繁体中文",
"locale.ja": "日语",
"index.title": "Hello i18n",
"index.home": "Home",
"index.component": "Component",
"index.api": "API",
"index.schema": "Schema",
"index.demo": "uni-app globalization",
"index.demo-description": "Include uni-framework, manifest.json, pages.json, tabbar, Page, Component, API, Schema",
"index.detail": "Detail",
"index.language": "Language",
"index.language-info": "Settings",
"index.system-language": "System language",
"index.application-language": "Application language",
"index.language-change-confirm": "Applying this setting will restart the app",
"api.message": "Message",
"schema.name": "Name",
"schema.add": "Add",
"schema.add-success": "Add success",
"tabbar.home": "HOME",
"tabbar.book": "BOOK",
"tabbar.unlock": "UNLOCK",
"tabbar.personal": "PERSONAL",
"home.select": "SELECT A STORE",
"home.reserve": "RESERVE",
"home.book": "BOOK NOW",
"home.detail": "DETAILS",
"home.appointment": "APPOINTMENT",
"home.recommend": "RECOMMENDED",
"home.morestore": "MORE STORES",
"home.navigate": "NAVIGATE",
"home.wayfinding": "ROUTE GUIDANCE",
"home.travel": "Travel",
"home.collection": "Collection",
"home.clothes": "Clothes",
"home.appliances": "Appliances",
"home.goods": "Goods",
"home.relocate": "Relocation",
"home.wrapper": "Packing",
"home.material": "Materials",
"home.document": "Document",
"home.device": "Equipment",
"home.supplies": "Supplies",
"home.ecgoods": "E-Commerce",
"home.shops": "RETAIL SHOPS",
"home.individuals": "INDIVIDUALS",
"home.corporates": "CORPORATES",
"home.advantage1": "Guarantee",
"home.advantage1Info1": "Chain Management",
"home.advantage1Info2": "Extensive media coverage",
"home.advantage1Info3": "Best Choice",
"home.advantage2": "Convenience",
"home.advantage2Info1": "24hrs Self-Access",
"home.advantage2Info2": "Smart Lock",
"home.advantage2Info3": "Flexible Term",
"home.advantage3": "Safety",
"home.advantage3Info1": "24hrs CCTV",
"home.advantage3Info2": "One Door One Lock",
"home.advantage3Info3": "Free Insurance",
"home.advantage4": "Cleanliness",
"home.advantage4Info1": "Environment Control",
"home.advantage4Info2": "Fire Safety System",
"home.advantage4Info3": "Regular Sterilization",
"home.interior": "Interior",
"home.serviceHotline": "Customer Service Hotline",
"home.quote": "Quote Now",
"book.location": "LOCATION",
"book.map": "MAP MODE",
"book.list": "LIST MODE",
"book.city": "City",
"book.area": "Area",
"book.get": "Find your nearest store",
"book.getSite": "You need to turn on the location Settings to show the nearest store",
"book.getCode": "Get QR code",
"unlock.door": "DOOR",
"unlock.lock": "LOCK",
"unlock.renew": "RENEW",
"unlock.details": "DETAILS",
"unlock.moveout": "MOVE OUT",
"unlock.cancelPending": "Pending For Move-out Approval",
"unlock.outComplete": "Move-out Completed",
"unlock.cancel": "Cancel",
"unlock.cCancel": "CANCEL",
"unlock.cancelout": "Cancel Move-out Request",
"unlock.return": "RETURN",
"unlock.moveoutReminder": "MOVE OUT REMINDER",
"unlock.moveoutTip": "After you finished the termination procedures, your entrance access will be removed. Please make sure your belongings are taken away. Deposit will be returned in 14 working days.",
"unlock.uploadTip": "Please take a picture of an empty storage unit and upload it.",
"unlock.moveoutSuccess": "Warehouse return request is in process, please patiently wait for staff review!",
"unlock.confirmOut": "MOVE OUT",
"unlock.evaluate": "EVALUATE",
"unlock.disapproval": "DISAPPROVAL",
"unlock.disapprovalRemarks": "Disapproval Remarks",
"unlock.overdue": "OVERDUE",
"unlock.unPaid": "UNPAID",
"unlock.order": "MY ORDER",
"unlock.login": "Click To Login",
"unlock.nodata": "NO DATA",
"unlock.auth": "Auth",
"unlock.FixedPassword": "Fixed Password",
"unlock.AccessControlCardBinding": "Access Control Card Binding",
"unlock.LockCardUnbinding": "Lock Card Binding",
"unlock.getAuthOrder": "View authorized orders",
"unlock.ResetPassword": "Reset Password",
"unlock.remoteOpen": "Remote Access",
"unlock.remoteOpenLoading": "Door Opening...",
"unlock.remoteOpenSuccess": "Access Granted",
"unlock.remoteOpenFail": "Access Denied",
"unlock.fillInventory": "Please fill in the inventory list before using the warehouse",
"unlock.FaceEnrollment": "Enrollment",
"unlock.goToPay": "Go to Pay",
"unlock.cancelOrder": "Cancel Order",
"unlock.Deposit Refund": "Deposit Refund",
"unlock.agreement": "Agreement",
"bingCard.start": "Start pairing",
"bingCard.fail": "The operation failed",
"bingCard.Click": "Click [ Start Pairing ]",
"bingCard.Pairing": "Pairing",
"bingCard.panel": "Place the door card on the dashboard",
"bingCard.single": "You will hear a beep sound when card is paired successfully.",
"bingCard.close": "After success, you can close this pop-up window manually.",
"detail.store": "Store",
"detail.unit": "Unit type",
"detail.spec": "Ref Spec",
"detail.size": "Ref Size",
"detail.cSize": "Ref Volume",
"detail.cUnit": "UNIT TYPE",
"detail.startDate": "START DATE",
"detail.lease": "LEASE TERM",
"detail.rentalFee": "Rental fee",
"detail.cDeposit": "Deposit(1 month)",
"detail.cValueAdded": "VALUE-ADDED",
"detail.nodemand": "No Demand",
"detail.coupon": "COUPON",
"detail.valuation": "ITEM VALUATION",
"detail.currency": "(Currency: RMB)",
"detail.extraTip": "If your belongings exceed a valuation of $5000, please insure the premium yourself.",
"detail.feeDetail": "FEE DETAILS",
"detail.deposit": "Deposit",
"detail.valueAdded": "Value-added",
"detail.discount": "Discount",
"detail.total": "Total",
"detail.next": "NEXT",
"detail.read": "I have read and agreed on ",
"detail.agreement": "[User Service Agreement]",
"detail.agreeTip": "Please read and agree on [User Service Agreement].",
"detail.orderNum": "ORDER NUMBER",
"detail.type": "UNIT TYPE",
"detail.period": "LEASE PERIOD",
"detail.click": "Details: Click on",
"detail.sitemap": "SITEMAP",
"detail.selected": "SELECTED",
"detail.nodata": "No Data",
"detail.noselect": "No Select",
"detail.RENEWAL": "RENEWAL",
"detail.RefundableDeposit": "Refundable Deposit",
"detail.to": "TO",
"detail.quotation": "Quotation",
"detail.generateQuotation": "Generate Quotation",
"detail.regenerateQuotation": "Re-Generate Quotation",
"detail.viewQuotation": "View Quotation",
"detail.quotationFail": "Quotation generation failed",
"detail.quotationSuccess": "Please manually save or forward the quotation",
"detail.agreeTerm": "Agree to Terms",
"detail.scrollRead": "Please read all terms before agreeing",
"detail.points": "Points",
"detail.PointsRedemption": "Points Redemption",
"detail.AvailablePoints": "Available Points",
"detail.DeductionAmount": "Deduction Amount",
"door.refresh": "Refresh QR code",
"door.refreshPwd": "Refresh Password",
"door.tip": "Use facial recgonition device to scan QR code.",
"door.valid": "Valid for 1 minute.",
"door.pwd": "Enter the password to unlock.",
"door.Unlock": "Unlock",
"door.UnlockSuccessful": "Unlock Successful",
"person.order": "Order details",
"person.promotion": "Promotion",
"person.identify": "Identification",
"person.invoice": "Invoice",
"person.guide": "User guide",
"person.customer": "Customer service",
"person.invitation": "Invitation",
"person.evaluation": "Evaluation",
"person.latestEvents": "Latest Events",
"person.lock": "Locks",
"person.share": "SHARE AND GAIN RENT-GREE",
"person.join": "JOIN NOW",
"person.VideoTutorial": "Tutorial",
"person.referrerInfo": "Referrer Info",
"site.branch": "BRANCH",
"site.address": "Address",
"site.tip": "*illegal items are strictly prohibited",
"site.tip2": "*Charges are based on internal dimensions only",
"site.ReferenceVolume": "Ref Volume",
"site.WarehouseInternalDimensions": "Int. Dims",
"site.full": "SORRY,THIS TYPE IS FULL,PLEASE SELECT ANOTHER TYPE.",
"site.appointment": "Appointment",
"site.appointmentSuccess": "Appointment successful. Our staff will contact you shortly, please keep your phone accessible!",
"site.hadAppointment": "Already Appointment",
"site.cancelAppointment": "Whether to cancel the reservation",
"site.noAccessId": "The accesscontrol ID is not available",
"site.noPermission": "There is no permission for this store",
"login.account": "account",
"login.password": "password",
"login.confirm": "confirm password",
"login.code": "code",
"login.input": "Please input",
"login.login": "Log in",
"login.wxLogin": "WeChat Login",
"login.register": "Register an account",
"login.forget": "forget the password?",
"login.send": "SEND",
"login.change": "Change",
"login.toLogin": "LOGIN AN ACCOUNT",
"login.registered": "Registered",
"login.different": "The password is different.",
"login.phone": "Phone Number",
"login.inputPhone": "Please enter your phone number",
"login.phoneFormat": "Invalid phone format",
"login.inputCode": "Please enter the code",
"login.getCode": "Get Code",
"login.sending": "Sending...",
"login.sendSuccess": "Code sent",
"login.UserAgreement": "《User Agreement》",
"login.andAgreeTo": "and agree to",
"request.tip": "Tips",
"request.cancel": "Cancel",
"request.confirm": "Confirm",
"request.loginContent": "No login, whether to jump to the login page",
"request.captchaError": "CAPTCHA error or expired",
"request.userCancel": "Request canceled by user",
"request.timeout": "Network request timed out",
"request.noConnect": "Failed to connect to server",
"request.error": "Error",
"toast.copy": "Successful replication",
"invite.title1": "Invite friends and",
"invite.title2": "win the rewards.",
"invite.number": "Invitations",
"invite.activity": "Mechanism",
"invite.branch": "Branch",
"invite.details": "Details",
"invite.toInvite": "INVITE NOW",
"invite.record": "View records",
"invite.disclaimer": "* Disclaimer:",
"invite.disContent": " the sole decision of Storage Limited shall be final in case of any dispute.",
"common.edit": "Edit",
"common.paySuccess": "Payment Successful",
"common.payFail": "Payment Failed, Please Try Again!",
"common.$": "$",
"common.notStarted": "Not Started",
"common.status": "Status",
"common.verifyInfo": " Info Verify",
"common.infoUpdate": "Info Update",
"common.saveInfo": "Save Info",
"common.personalAuth": "Personal Auth",
"common.businessAuth": "Business Auth",
"common.IdCardFont": "Upload ID Front",
"common.IdCardBack": "Upload ID Back",
"common.UploadBusinessLicense": "Upload Business License",
"common.noOpen": "Not open, contact staff for details!",
"common.isGoAuth": "Upload personal information for verification.",
"common.Authentication": "Authentication",
"common.submit": "Submit",
"common.placeInputAll": "Please enter all the information",
"common.goodsList": "Goods List",
"common.OnlineConsultation": "Consultation",
"common.avatar": "Avatar",
"common.uploadAvatar": "Upload Avatar",
"common.nickname": "Nickname",
"common.phone": "Phone",
"common.bindPhone": "Bind Phone",
"common.bindPhoneAfter": "Bind the mobile phone number before performing this operation",
"common.bindPhoneUnlock": "Bind the phone number to get the order information",
"common.QuickBind": "Quick Bind",
"common.cancelBind": "Cancel Bind",
"common.facialData": "Facial Data",
"common.auth": "Authorization",
"common.requireAvatar": "Please upload the profile picture",
"common.requireName": "Please enter the nickname",
"common.requirePhone": "Please enter the phone number",
"common.note": "Notes",
"common.tip": "Tip",
"common.cancelApply": "Are you sure to cancel the application?",
"common.cancelSuccess": "Cancel successfully",
"common.cancelFail": "Cancel failed. Please try again later!",
"common.addOrder": "Confirm Order",
"common.AuthenticationFailedTips": "Your identity verification has not been approved.You may still place an order for same-day warehouse use (within the rental period).However, starting from the next day, you must submit valid identity information and pass verification to continue using the warehouse.",
"common.VacantDay": "Vacant Day",
"common.RemainingDay": "Remaining Day",
"common.OverdueDay": "Overdue Day",
"common.ORDER_AMOUNT_ERROR": "Invalid discount amount. Please select again.",
"common.tuangouCouponPrice": "tuangou coupon price",
"common.checkAgreementUrl": "Please read and agree to the terms first.",
"common.Expand": "Expand",
"common.Collapse": "Collapse",
"common.OtherStores": "Other Stores",
"coupon.coupon": "Coupon",
"coupon.meituanOrdazhongdianpingCoupon": "Meituan/Dazhongdianping Coupon",
"coupon.queryMeituanDazhongdianpingCoupon": "Click to check Meituan/Dianping coupons",
"coupon.useTips": "Instruction: Enter the coupon code to enjoy the discount.",
"coupon.enterCode": "Coupon code",
"coupon.limitedtimeoffer": "Limited-time offer",
"coupon.storewide": "Store-wide",
"coupon.redeemNow": "Redeem",
"coupon.instructions": "Instructions",
"coupon.validityPeriod": "Expiry",
"coupon.apply": "Apply",
"coupon.all": "All Unit Types",
"coupon.multiStoreUse": "Multi-store use",
"coupon.renewable": "Usable on reorders",
"coupon.noRenewable": "Not usable on reorders",
"coupon.redemptionuccessful": "Redemption successful",
"coupon.unusableCoupons": "Unusable Coupons",
"coupon.currentConditionsNotMet": "Current conditions not met",
"request.promoCodeError": "Promo code error",
"validation.getPhoneFail": "Failed to obtain the phone number, please enter it manually",
"validation.inputName1": "Please input the name",
"validation.selectCardType": "Please select the card type",
"validation.inputIdCard": "Please input the ID number",
"validation.uploadIdCard": "Please upload your ID photo",
"validation.inputPhone": "Please input the phone number",
"validation.inputInternationalPhone": "Please input the international phone number",
"validation.uploadImg": "The picture is uploading, please try again later",
"validation.inputName2": "Please input the company name",
"validation.inputLicense": "Please input the business license number",
"validation.uploadLicense": "Please upload your business ID card",
"validation.submitSuccess": "Submit successfully",
"validation.uploadSuccess": "Update successfully",
"validation.identifyCard": "China ID Card",
"validation.passport": "Passport",
"validation.permit": "Hong Kong and Macao Permit",
"validation.access": "One-click Access",
"validation.bind": "Binding",
"validation.vailSuccess": "Verification successful.",
"validation.agree": "Please read and agree to the User Service Agreement and Privacy Policy.",
"agreement.readAndAgree": "I have read and agree to the",
"agreement.service": "User Service Agreement",
"agreement.and": "and",
"agreement.privacy": "Privacy Policy",
"agreement.toast": "Please read and agree to the User Service Agreement and Privacy Policy.",
"verification.vailFail": "Authentication failed. Please verify that the document type and information are correct, and re-upload a clear image (front and back). Try authenticating again!",
"verification.vailSuccess": "Authentication successful.",
"invoiceApply.electronicInvoice": "Electronic Invoice",
"invoiceApply.paperInvoice": "Paper Invoice",
"invoiceApply.invoiceTips": "Your invoice application has been submitted successfully, please wait patiently for the staff to contact!",
"invoice.valid": "Invoices",
"invoice.pay": "Payment Time",
"invoice.site": "Store",
"invoice.type": "Unit Type",
"invoice.unit": "Unit",
"invoice.rent": "Lease Term",
"invoice.record": "Application Record",
"invoice.allSelect": "Select All",
"invoice.nextStep": "NEXT",
"invoice.tip": "- Invoices can be issued within one month after payment of the order",
"invoice.serial": "Number",
"invoice.time": "Application Time",
"invoice.status": "Audit Status",
"invoice.status0": "Pending approval",
"invoice.status1": "Approved, invoicing...",
"invoice.status2": "Failed approval",
"invoice.status3": "Cancelled",
"invoice.status4": "Invoiced",
"invoice.selectOrder": "Please select an order",
"invoice.validMoney": "The order amount must be greater than 0",
"evaluate.customerEvaluation": "CUSTOMER EVALUATION",
"evaluate.overallRating": "Overall rating",
"evaluate.userExperience": "User experience",
"evaluate.Hospitality": "Hospitality",
"evaluate.cleanliness": "Cleanliness",
"evaluate.convenience": "Convenience",
"evaluate.tips": "Please leave your invaluable comment or suggestion here.",
"evaluate.anonymous": "ANONYMOUS",
"goodsList.note": "Please note",
"goodsList.info": "Hello, according to the requirements of relevant departments, the items you store need to be declared independently for their category. This declaration form is for record keeping purposes, please fill it out carefully.",
"goodsList.multi": "Multiple choices",
"goodsList.tip1": "User Confirmation",
"goodsList.tip2": "1. The stored items were obtained through legal channels;",
"goodsList.tip3": "2. Do not store prohibited items;",
"goodsList.tip4": "3. If property damage or personal injury is caused by changes in the user's stored items or other reasons, the user shall bear the responsibility.",
"goodsList.submit": "Confirm",
"houseKey.FriendsName": "Friend's name",
"houseKey.AuthorizationDate": "Authorization date",
"houseKey.ReceiveNotifications": "Receive notifications",
"houseKey.EnableNotifications": "Enable notifications",
"houseKey.PhoneNumber": "Phone number",
"houseKey.email": "Email",
"houseKey.EnterAuthorizedPhoneNumber": "Enter authorized person's phone number",
"houseKey.AddAuthorization": "Add authorization",
"houseKey.EnterFriendsName": "Enter friend's name",
"houseKey.EnterAuthorizationDate": "Enter authorization date",
"houseKey.EnterPhoneNumber": "Enter phone number",
"houseKey.EnterEmail": "Enter email",
"houseKey.CannotAuthorizeYourself": "Cannot authorize yourself",
"houseKey.UpdateSuccessful": "Update successful",
"houseKey.AddedSuccessfully": "Added successfully",
"houseKey.date": "Year-Month-Day",
"houseKey.overdue": "The authorization period has expired. Please re-select the authorization period and renew it.",
"houseKey.otherPhone": "The phone number of the authorized party",
"houseKey.otherEmail": "The email address of the authorized party",
"houseKey.getNote": "Open to receive notifications",
"unitTypeDetail.oneMonth": "Lease term: one month",
"unitTypeDetail.reference": "Reference",
"unitTypeDetail.discount": "Discount",
"reserve.FULLNAME": "Full Name",
"reserve.PHONE": "Phone",
"reserve.REGION": "Region",
"reserve.TYPE": "Type",
"reserve.PHONE NUMBER": "Phone Number",
"reserve.Individual & Family": "Individual & Family",
"reserve.Business & E-commerce": "Business & E-commerce",
"reserve.Retail & Store": "Retail & Store",
"reserve.contentTips": "Appointment successful! Please wait patiently for our staff to contact you. Keep your phone available. Thank you!",
"inviteDetail.title": "Invitation Records",
"inviteDetail.Username": "Username",
"inviteDetail.Registration Date": "Registration Date",
"inviteDetail.Status": "Status",
"inviteDetail.No invitation": "No invitation records found. Please share invitations.",
"inviteDetail.SORRY": "SORRY, THERE ARE NO RECORDS. PLEASE SHARE AND INVITE.",
"inviteDetail.Share Invitation": "Share Invitation",
"referrerInfo.company": "Referrer Company",
"referrerInfo.branch": "Referrer Branch",
"referrerInfo.commission": "Referral Commission Rate",
"referrerInfo.inviteRegister": "Invitation to Register",
"referrerInfo.inviteRecord": "Invitation Record",
"referrerInfo.inviteUserName": "User Name",
"referrerInfo.invitePhone": "Phone Number",
"referrerInfo.registrationTime": "Registration Time",
"referrerInfo.inviteEmpty": "No invitation record",
"referrerInfo.loadQrCode": "Download QR Code",
"referrerInfo.loadPoster": "Download Poster",
"referrerInfo.forwardInvitation": "Forward Invitation",
"pointsMall.title": "Points Mall",
"pointsMall.myPoints": "My Points",
"pointsMall.pointsUnit": "Points",
"pointsMall.exchange": "Redeem",
"pointsMall.exchangeFormTitle": "Shipping Information",
"pointsMall.submit": "Submit",
"pointsMall.receiverName": "Recipient Name",
"pointsMall.phone": "Phone Number",
"pointsMall.address": "Address",
"pointsMall.placeholderName": "Enter recipient name",
"pointsMall.placeholderPhone": "Enter phone number",
"pointsMall.placeholderAddress": "Enter detailed shipping address",
"pointsMall.exchangeConfirmTitle": "Exchange Confirmation",
"pointsMall.exchangeConfirmTip": "Are you sure you want to use {points} points to redeem?",
"pointsMall.stock": "Stock",
"pointsMall.successTitle": "Success",
"pointsMall.successTip": "Redemption successful. Please wait for our staff to contact you. Thank you!",
"pointsMall.toastOutOfStock": "Out of stock",
"pointsMall.toastNotEnoughPoints": "Not enough points",
"pointsMall.toastExchanging": "Redeeming...",
"pointsMall.toastExchangeFailed": "Redemption failed",
"pointsMall.exchangeRecordTitle": "Redemption Records",
"pointsMall.noExchangeRecord": "No redemption records"
}

38
locale/index.js Normal file
View File

@ -0,0 +1,38 @@
import { createI18n } from 'vue-i18n';
// 导入静态翻译内容
import en from './en.json';
import zhHans from './zh-Hans.json';
import zhHant from './zh-Hant.json';
import ja from './ja.json';
// 导入动态翻译函数
import messagesFunctions from './messagesFunctions.js';
// 合并静态和动态的翻译内容
const mergedMessages = {
en: {
...en,
...messagesFunctions.en,
},
'zh-Hans': {
...zhHans,
...messagesFunctions.zhHans,
},
'zh-Hant': {
...zhHant,
...messagesFunctions.zhHant,
},
ja: {
...ja,
...messagesFunctions.ja,
},
};
const language = "zh-Hans";
const i18n = createI18n({
locale: language,
messages: mergedMessages
});
uni.setStorageSync("eliteSys-language-wx", language);
export default i18n;

23
locale/ja.json Normal file
View File

@ -0,0 +1,23 @@
{
"locale.auto": "システム",
"locale.en": "英語",
"locale.zh-hans": "简体中文",
"locale.zh-hant": "繁体中文",
"locale.ja": "日语",
"index.title": "Hello i18n",
"index.home": "ホーム",
"index.component": "コンポーネント",
"index.api": "API",
"index.schema": "Schema",
"index.demo": "uni-app globalization",
"index.demo-description": "ユニフレームワーク、manifest.json、pages.json、タブバー、ページ、コンポーネント、APIを含める、Schema",
"index.detail": "詳細",
"index.language": "言語",
"index.language-info": "設定",
"index.system-language": "システム言語",
"index.application-language": "アプリケーション言語",
"index.language-change-confirm": "この設定を適用すると、アプリが再起動します",
"api.message": "メッセージ",
"schema.add": "追加",
"schema.add-success": "成功を追加"
}

View File

@ -0,0 +1,49 @@
const numToChinese = (num) => {
if(num<1) num = 1
const map = ['零','一','二','三','四','五','六','七','八','九']
return map[num] || num.toString()
}
export default {
en: {
'person.inviteData': ({ named }) => `Already invited ${named('friends')} friends, Opportunity to receive additional rewards`,
"detail.remain": ({ named }) => `Remaining ${named('days')} days`,
"detail.discountOff": ({ named }) => `${named('month')} MONTHS ${named('percent')}% OFF`,
"month": ({ named }) => `${named('count')} month(s)`,
"months": ({ named }) => `${named('count')} month(s)`,
"discountMomey": ({ named }) => `${named('discount')} Discount `,
"firstMonthRent": ({ named }) => `First Month Rent ${named('discount')}`,
"couponDiscount": ({ named }) => `${named('percent')} off `,
"freeMonth": ({ named }) => `Free ${named('discount')} months`,
"BonusMonth": ({ named }) => `Bonus ${named('discount')} months`,
"requiredMomey": ({ named }) => `${named('momey')} required`,
"fullMonths": ({ named }) => `available after ${named('count')} full months`,
"invoice.order": ({ named }) => `${named('number')} orders have been selected, totaling ${named('money')} yuan`,
"giftMonth": ({ named }) => `Gifted ${named('count')} months`,
"storeRenovationNotice": ({ named }) => `This store is under renovation. It will be available on ${named('limitDate')}. Orders can be placed in advance.`,
"storeCount": ({ named }) => `${named('count')} stores`,
"discount": ({ named }) => `${Math.floor(named('discount') * 100)}% off`,
"flashSaleDiscount": ({ named }) => `Extra ${Math.floor(named('discount') * 100)}% off`,
'order.confirmReferrer': ({ named }) => `This order will be associated with the referrer ${named('referrer')}. Please confirm whether this transaction was completed through their referral.`,
},
zhHans: {
'person.inviteData': ({ named }) => `已邀请 ${named('friends')} 名好友, 有机会获得额外奖励`, //获得 ${named('days')} 天免费租期!
"detail.remain": ({ named }) => `租期:剩余${named('days')}`,
"detail.discountOff": ({ named }) => `${named('month')}个月享${named('discount')}`,
"month": ({ named }) => `${named('count')}`,
"months": ({ named }) => `${named('count')}`,
"discountMomey": ({ named }) => `优惠 ¥${named('discount')} `,
"firstMonthRent": ({ named }) => `首月 ${named('discount')}`,
"couponDiscount": ({ named }) => `${named('discount')}`,
"freeMonth": ({ named }) => `免租 ${named('discount')} 个月`,
"BonusMonth": ({ named }) => `赠送 ${named('discount')} 个月`,
"requiredMomey": ({ named }) => `满¥${named('momey')} 可用`,
"fullMonths": ({ named }) => `${named('count')} 个月可用`,
"invoice.order": ({ named }) => `已选 ${named('number')} 个订单,共 ${named('money')}`,
"giftMonth": ({ named }) => `赠送 ${named('count')} 个月`,
"storeRenovationNotice": ({ named }) =>`该店铺装修中,将于 ${named('limitDate')} 开放使用,可提前下单`,
"storeCount": ({ named }) => `一共 ${named('count')}`,
"discount": ({ named }) => `${numToChinese(Math.floor(named('discount') * 10))||numToChinese[1]}`,
"flashSaleDiscount": ({ named }) => `额外${numToChinese(Math.floor(named('discount') * 10))}`,
'order.confirmReferrer': ({ named }) => `此订单将关联至转介人 ${named('referrer')}。请确认是否通过他的推荐完成本次交易?`,
},
};

36
locale/uni-app.ja.json Normal file
View File

@ -0,0 +1,36 @@
{
"common": {
"uni.app.quit": "もう一度押すと、アプリケーションが終了します",
"uni.async.error": "サーバーへの接続がタイムアウトしました。画面をクリックして再試行してください",
"uni.showActionSheet.cancel": "キャンセル",
"uni.showToast.unpaired": "使用するには、showToastとhideToastをペアにする必要があることに注意してください",
"uni.showLoading.unpaired": "使用するには、showLoadingとhideLoadingをペアにする必要があることに注意してください",
"uni.showModal.cancel": "キャンセル",
"uni.showModal.confirm": "OK",
"uni.chooseImage.cancel": "キャンセル",
"uni.chooseImage.sourceType.album": "アルバムから選択",
"uni.chooseImage.sourceType.camera": "カメラ",
"uni.chooseVideo.cancel": "キャンセル",
"uni.chooseVideo.sourceType.album": "アルバムから選択",
"uni.chooseVideo.sourceType.camera": "カメラ",
"uni.previewImage.cancel": "キャンセル",
"uni.previewImage.button.save": "画像を保存",
"uni.previewImage.save.success": "画像をアルバムに正常に保存します",
"uni.previewImage.save.fail": "画像をアルバムに保存できませんでした",
"uni.setClipboardData.success": "コンテンツがコピーされました",
"uni.scanCode.title": "スキャンコード",
"uni.scanCode.album": "アルバム",
"uni.scanCode.fail": "認識に失敗しました",
"uni.scanCode.flash.on": "タッチして点灯",
"uni.scanCode.flash.off": "タップして閉じる",
"uni.startSoterAuthentication.authContent": "指紋認識...",
"uni.picker.done": "完了",
"uni.picker.cancel": "キャンセル",
"uni.video.danmu": "「弾幕」",
"uni.video.volume": "ボリューム",
"uni.button.feedback.title": "質問のフィードバック",
"uni.button.feedback.send": "送信"
},
"ios": {},
"android": {}
}

502
locale/zh-Hans.json Normal file
View File

@ -0,0 +1,502 @@
{
"common.confirm": "确认",
"common.cancel": "取消",
"common.title": "提示",
"common.noData": "暂无数据",
"common.delete": "删除",
"common.update": "更新",
"common.password": "密码",
"common.close": "关闭",
"common.Skiptoday": "暂时跳过",
"common.payableTime": "剩余支付时间",
"common.AuthorizationOrder": "授权订单",
"common.cancelOrder": "取消订单",
"common.cancelOrderTips": "确认取消当前订单?",
"common.unpaidOrderTips": "存在未支付订单,请先支付,或者取消订单后再下单!",
"common.cantUselocker": "此体积仓位不可租用,请重新选择!",
"common.checkAgreementUrl": "请先阅读并同意协议",
"common.logout": "登出",
"common.logoutTip": "确认退出程序?",
"common.FirstTimeLoginTips": "「首次登录将自动注册」",
"common.SpaceSpecsGuide": "空间规格介绍参考",
"common.Seelegend": "查看图例",
"common.Rent": "租用",
"common.OriginalPrice": "原价格",
"common.SalePrice": "活动价",
"common.SwitchStores": "切换同地址分店",
"common.more": "更多",
"common.SwitchRegion": "切换区域",
"common.Countdown": "倒计时",
"common.FlashSale": "限时抢购",
"common.FlashSalePrice": "限时抢购价",
"common.ClickToCheck": "点击查看",
"common.ClickToZoomIn": "点击放大",
"common.tryZooming": "如果门禁识别不成功,可尝试点击放大",
"common.OnSiteAssessment": "上门评估",
"common.ConsultationQuotation": "咨询报价",
"common.Requirement": "需求",
"common.day": "天",
"common.reset": "重置",
"common.notRented": "未租",
"common.rented": "已租",
"common.locked": "鎖定",
"locale.auto": "系统",
"locale.en": "English",
"locale.zh-hans": "简体中文",
"locale.zh-hant": "繁体中文",
"locale.ja": "日语",
"index.title": "Hello i18n",
"index.home": "主页",
"index.component": "组件",
"index.api": "API",
"index.schema": "Schema",
"index.demo": "uni-app 国际化演示",
"index.demo-description": "包含 uni-framework、manifest.json、pages.json、tabbar、页面、组件、API、Schema",
"index.detail": "详情",
"index.language": "语言",
"index.language-info": "语言信息",
"index.system-language": "系统语言",
"index.application-language": "应用语言",
"index.language-change-confirm": "应用此设置将重启App",
"api.message": "提示",
"schema.name": "姓名",
"schema.add": "新增",
"schema.add-success": "新增成功",
"tabbar.home": "首页",
"tabbar.book": "订仓",
"tabbar.unlock": "用仓",
"tabbar.personal": "我的",
"home.select": "选择分店",
"home.reserve": "立即预约",
"home.book": "立即订仓",
"home.detail": "查看详情",
"home.appointment": "立即预约",
"home.recommend": "热推门店",
"home.morestore": "所有门店",
"home.navigate": "导航",
"home.wayfinding": "路径指引",
"home.travel": "出差寄存",
"home.collection": "玩具收藏",
"home.clothes": "衣物鞋帽",
"home.appliances": "家具家电",
"home.goods": "门店货物",
"home.relocate": "搬迁装修",
"home.wrapper": "包装材料",
"home.material": "物资储备",
"home.document": "文件档案",
"home.device": "办公设备",
"home.supplies": "活动物资",
"home.ecgoods": "电商货品",
"home.shops": "零售·门店",
"home.individuals": "个人·家庭",
"home.corporates": "企业·电商",
"home.advantage1": "连锁经营",
"home.advantage1Info1": "实力保证 覆盖广深",
"home.advantage1Info2": "南方卫视 多家采访",
"home.advantage1Info3": "千万用户 最优选择",
"home.advantage2": "使用方便",
"home.advantage2Info1": "全天开放 随时存取",
"home.advantage2Info2": "手机开仓 手机开锁",
"home.advantage2Info3": "一个月起 即租即用",
"home.advantage3": "安全保障",
"home.advantage3Info1": "实时监控 无死角位",
"home.advantage3Info2": "一人一仓 独立储物",
"home.advantage3Info3": "免费保险 保驾护航",
"home.advantage4": "环境整洁",
"home.advantage4Info1": "温度调节 防虫防鼠",
"home.advantage4Info2": "配备消防 光洁明亮",
"home.advantage4Info3": "定期保洁 专业消毒",
"home.interior": "参照",
"home.serviceHotline": "客服热线",
"home.quote": "立即查价",
"book.location": "地区筛选",
"book.map": "地图模式",
"book.list": "列表模式",
"book.city": "城市",
"book.area": "区域",
"book.get": "查找离你最近的店铺",
"book.getSite": "需要打开位置信息设置,来显示最近店铺",
"book.getCode": "获取二维码",
"unlock.door": "开门",
"unlock.lock": "开锁",
"unlock.renew": "续仓",
"unlock.details": "详情",
"unlock.moveout": "退仓",
"unlock.cancelPending": "退仓申请中",
"unlock.outComplete": "退仓完成",
"unlock.cancel": "取消",
"unlock.cCancel": "取消",
"unlock.cancelout": "取消退仓请求",
"unlock.return": "返回",
"unlock.moveoutReminder": "退仓提示",
"unlock.moveoutTip": "退仓后您将失去开门权限请确认已经清空仓内物品14个工作日内系统将会自动返还押金。",
"unlock.uploadTip": "请上传清空仓库后的照片。",
"unlock.moveoutSuccess": "退仓申请中,请等待耐心工作人员审核!",
"unlock.confirmOut": "确定退仓",
"unlock.evaluate": "评价",
"unlock.disapproval": "退仓驳回",
"unlock.disapprovalRemarks": "驳回理由",
"unlock.overdue": "已逾期",
"unlock.unPaid": "未支付",
"unlock.order": "我的订单",
"unlock.login": "点击登录,查看下单仓库",
"unlock.nodata": "暂无数据",
"unlock.auth": "授权",
"unlock.FixedPassword": "固定密码",
"unlock.AccessControlCardBinding": "门禁绑卡",
"unlock.LockCardUnbinding": "锁绑卡",
"unlock.getAuthOrder": "查看授权订单",
"unlock.ResetPassword": "重置密码",
"unlock.remoteOpen": "远程开门",
"unlock.remoteOpenLoading": "开门中...",
"unlock.remoteOpenSuccess": "开门成功",
"unlock.remoteOpenFail": "开门失败",
"unlock.fillInventory": "用仓前,请先填写物品清单",
"unlock.FaceEnrollment": "录入门禁",
"unlock.goToPay": "去支付",
"unlock.cancelOrder": "取消订单",
"unlock.Deposit Refund": "押金退款",
"unlock.agreement": "订仓协议",
"bingCard.start": "开始配对",
"bingCard.fail": "操作失败",
"bingCard.Click": "点击[ 开始配对 ]",
"bingCard.Pairing": "配对中",
"bingCard.panel": "请将ID卡开放在数位面板上",
"bingCard.single": "滴一声证明成功",
"bingCard.close": "成功后,可手工关闭此弹窗",
"detail.store": "门店",
"detail.unit": "仓型",
"detail.spec": "参考尺寸",
"detail.size": "参考体积",
"detail.cSize": "参考体积",
"detail.cUnit": "已选仓型",
"detail.startDate": "启用日期",
"detail.lease": "租赁期限",
"detail.rentalFee": "租仓费用",
"detail.cDeposit": "押金费用(一个月)",
"detail.cValueAdded": "额外服务",
"detail.nodemand": "无需求",
"detail.coupon": "使用优惠",
"detail.valuation": "费用详情",
"detail.currency": "(单位: 人民币)",
"detail.extraTip": "若您的物品估算金额超出¥5000超出部分请您自行投保。",
"detail.feeDetail": "费用详情",
"detail.deposit": "押金费用",
"detail.valueAdded": "额外费用",
"detail.discount": "优惠抵扣",
"detail.total": "总共费用",
"detail.next": "下一步",
"detail.read": "我已经阅读并同意 ",
"detail.agreement": "《订仓协议》",
"detail.agreeTip": "请阅读并同意《订仓协议》",
"detail.orderNum": "订单序号",
"detail.type": "仓库",
"detail.period": "租赁时期",
"detail.click": "详情:点击查看订单",
"detail.sitemap": "平面图",
"detail.selected": "已选仓位",
"detail.nodata": "未选填需求",
"detail.noselect": "未选优惠",
"detail.RENEWAL": "续仓记录",
"detail.RefundableDeposit": "可退押金",
"detail.to": "续费至",
"detail.quotation": "报价单",
"detail.generateQuotation": "生成报价单",
"detail.regenerateQuotation": "重新生成报价单",
"detail.viewQuotation": "查看报价单",
"detail.quotationFail": "生成报价单失败",
"detail.quotationSuccess": "请手动保存或转发报价单",
"detail.scrollRead": "請在同意前閱讀所有條款",
"detail.agreeTerm": "同意條款",
"detail.points": "积分",
"detail.PointsRedemption": "积分抵扣",
"detail.AvailablePoints": "可用积分",
"detail.DeductionAmount": "抵扣金额",
"door.refresh": "刷新二维码",
"door.refreshPwd": "刷新密码",
"door.tip": "使用门禁设备扫描二维码",
"door.valid": "有效时限为1分钟",
"door.pwd": "输入密码即可开锁",
"door.Unlock": "开锁",
"door.UnlockSuccessful": "开锁成功",
"person.order": "订单详情",
"person.promotion": "优惠卡包",
"person.identify": "信息认证",
"person.invoice": "发票申请",
"person.guide": "用户指南",
"person.customer": "客服咨询",
"person.invitation": "邀请详情",
"person.evaluation": "用户评价",
"person.latestEvents": "最新活动",
"person.lock": "锁具管理",
"person.share": "分享即送免费租期",
"person.join": "立即参加",
"person.VideoTutorial": "视频教程",
"person.referrerInfo": "推荐人信息",
"site.branch": "分店",
"site.address": "地址",
"site.tip": "*非违规违禁物品均可存放",
"site.tip2": "*仅按仓内尺寸收费",
"site.ReferenceVolume": "参考体积",
"site.WarehouseInternalDimensions": "仓内尺寸",
"site.full": "抱歉,此仓型已租满,请选择其他仓型。",
"site.appointment": "候补预约",
"site.appointmentSuccess": "预约成功。随后工作人员会联系你,请保持手机顺畅!",
"site.hadAppointment": "已预约",
"site.cancelAppointment": "是否取消预约",
"site.noAccessId": "暂无门禁设备accesscontrol ID",
"site.noPermission": "暂无该门店的权限",
"login.account": "账号",
"login.password": "密码",
"login.confirm": "确认密码",
"login.code": "验证码",
"login.input": "请输入",
"login.login": "登录",
"login.wxLogin": "手机号快捷登录",
"login.register": "注册账号",
"login.forget": "忘记密码?",
"login.send": "发送",
"login.change": "更改",
"login.toLogin": "登录账号",
"login.registered": "注册",
"login.different": "密码不一致",
"login.phone": "手机号",
"login.inputPhone": "请输入手机号",
"login.phoneFormat": "手机号格式错误",
"login.inputCode": "请输入验证码",
"login.getCode": "获取验证码",
"login.sending": "发送中...",
"login.sendSuccess": "验证码已发送",
"login.UserAgreement": "《用户协议》",
"login.andAgreeTo": "同意并接受",
"request.tip": "提示",
"request.cancel": "取消",
"request.confirm": "确认",
"request.loginContent": "未登录,是否前往登录页面",
"request.captchaError": "验证码错误或已过期",
"request.userCancel": "用户取消请求",
"request.timeout": "网络请求超时",
"request.noConnect": "连接服务器失败",
"request.error": "错误",
"toast.copy": "复制成功",
"invite.title1": "邀好友",
"invite.title2": "赢优惠券",
"invite.number": "邀请人数",
"invite.activity": "活动内容",
"invite.branch": "适用门店",
"invite.details": "详情咨询",
"invite.toInvite": "去邀请",
"invite.record": "查看邀请记录",
"invite.disclaimer": "* 活动解释权归迷你仓所有",
"invite.disContent": "",
"common.edit": "编辑",
"common.paySuccess": "支付成功",
"common.payFail": "支付失败,请重新尝试!",
"common.$": "¥",
"common.notStarted": "未开始",
"common.status": "状态",
"common.verifyInfo": "信息认证",
"common.infoUpdate": "信息修改",
"common.saveInfo": "保存信息",
"common.personalAuth": "个人认证",
"common.businessAuth": "企业认证",
"common.IdCardFont": "上传证件照正面",
"common.IdCardBack": "上传证件照反面",
"common.UploadBusinessLicense": "上传营业执照",
"common.noOpen": "暂未开放,详细请联系工作人员!",
"common.isGoAuth": "所有用户必须进行身份验证。方可用仓。",
"common.Authentication": "身份验证",
"common.submit": "提交",
"common.placeInputAll": "请填写完信息后提交",
"common.goodsList": "物品清单",
"common.OnlineConsultation": "在线咨询",
"common.avatar": "头像",
"common.uploadAvatar": "上传头像",
"common.nickname": "昵称",
"common.phone": "手机号",
"common.facialData": "数据",
"common.bindPhone": "绑定手机号",
"common.bindPhoneAfter": "请先绑定手机号再进行此操作",
"common.bindPhoneUnlock": "请绑定手机号才能获取订单信息",
"common.QuickBind": "快速绑定",
"common.cancelBind": "暂不授权",
"common.auth": "授权",
"common.requireAvatar": "请上传头像",
"common.requireName": "请输入昵称",
"common.requirePhone": "请输入手机号",
"common.note": "备注",
"common.tip": "提示",
"common.cancelApply": "确定取消申请吗?",
"common.cancelSuccess": "取消成功",
"common.cancelFail": "取消失败,请稍后重试!",
"common.addOrder": "直接下单",
"common.AuthenticationFailedTips": "您当前身份信息认证未通过,下单当天可用仓(租赁期内),第二天起需要提交正确的身份信息且验证通过,方可继续用仓。",
"common.VacantDay": "空闲",
"common.RemainingDay": "剩余",
"common.OverdueDay": "逾期",
"common.ORDER_AMOUNT_ERROR": "订单金额必须大于0.01元,请重新选择!",
"common.userName": "姓名",
"common.tuangouCouponPrice": "团购优惠劵金额",
"common.Expand": "展开",
"common.Collapse": "收起",
"common.OtherStores": "其他门店",
"coupon.coupon": "优惠券",
"coupon.meituanOrdazhongdianpingCoupon": "美团/大众点评优惠劵",
"coupon.queryMeituanDazhongdianpingCoupon": "点击查询美团/大众点评优惠劵",
"coupon.useTips": "使用说明:填入优惠码兑换即可享受优惠。",
"coupon.enterCode": "填入优惠码",
"coupon.limitedtimeoffer": "限时优惠",
"coupon.storewide": "全店通用",
"coupon.redeemNow": "立即兑换",
"coupon.instructions": "使用说明",
"coupon.validityPeriod": "有效期",
"coupon.apply": "立即使用",
"coupon.all": "全部仓型",
"coupon.unusableCoupons": "当前门店不可用优惠券",
"coupon.multiStoreUse": "多店可用",
"coupon.renewable": "续单可用",
"coupon.noRenewable": "续单不可用",
"coupon.redemptionuccessful": "兑换成功",
"coupon.currentConditionsNotMet": "当前条件不满足",
"request.promoCodeError": "优惠码不正确",
"validation.getPhoneFail": "获取手机号失败,请手动输入",
"validation.inputName1": "请填写用户姓名",
"validation.selectCardType": "请选择证件类型",
"validation.inputIdCard": "请填写证件号码",
"validation.uploadIdCard": "请上传证件照",
"validation.inputPhone": "请填写手机号码",
"validation.inputInternationalPhone": "请填写境外号码",
"validation.inputInternationalPhoneHk": "请填写香港号码",
"validation.uploadImg": "图片正在上传中,请稍后重试",
"validation.inputName2": "请填写企业名称",
"validation.inputLicense": "请填写营业执照号码",
"validation.uploadLicense": "请上传企业证照",
"validation.submitSuccess": "提交成功",
"validation.uploadSuccess": "修改成功",
"validation.identifyCard": "内地身份证",
"validation.passport": "护照",
"validation.permit": "港澳通行证",
"validation.access": "一键获取",
"validation.bind": "一键绑定",
"validation.vailSuccess": "认证成功",
"agreement.readAndAgree": "我已阅读并同意",
"agreement.service": "《用户服务协议》",
"agreement.and": "及",
"agreement.privacy": "《隐私政策》",
"agreement.toast": "请先阅读并同意《用户服务协议》和《隐私政策》",
"verification.vailFail": "认证不通过,请检查证件类型与证件信息是否正确,请重新上传清晰图片(注意正反面)。",
"verification.vailSuccess": "认证成功",
"invoiceApply.electronicInvoice": "发票申请",
"invoiceApply.paperInvoice": "纸质发票",
"invoiceApply.invoiceTips": "您的发票申请已经提交成功,请耐心等候工作人员联系!",
"invoice.valid": "可开发票",
"invoice.pay": "支付时间",
"invoice.site": "门店",
"invoice.type": "仓型",
"invoice.unit": "仓位",
"invoice.rent": "租期",
"invoice.record": "申请记录",
"invoice.allSelect": "全选",
"invoice.nextStep": "下一步",
"invoice.tip": "- 订单付款后 一个月内可开发票",
"invoice.serial": "编号",
"invoice.time": "申请时间",
"invoice.status": "审核状态",
"invoice.status0": "待审核",
"invoice.status1": "审核通过,开票中",
"invoice.status2": "审核不通过",
"invoice.status3": "已取消",
"invoice.status4": "已开票",
"invoice.selectOrder": "请选择订单",
"invoice.validMoney": "订单金额必须大于0元",
"evaluate.customerEvaluation": "顾客评价",
"evaluate.overallRating": "综合评分",
"evaluate.userExperience": "用户体验",
"evaluate.Hospitality": "服务态度",
"evaluate.cleanliness": "整洁度",
"evaluate.convenience": "便利度",
"evaluate.tips": "请在这里留下你宝贵的意见或建议。",
"evaluate.anonymous": "匿名",
"goodsList.note": "请备注",
"goodsList.info": "您好,根据有关部门要求,您存放的物品需要进行物品品类自主申报。本申报单作留底备查之用,请认真填写。",
"goodsList.multi": "可多选",
"goodsList.tip1": "用户确认:",
"goodsList.tip2": "1、存放的物品均为合法渠道获取;",
"goodsList.tip3": "2、不存放违禁品;",
"goodsList.tip4": "3、由于用户存放物品异变等原因造成的财产毁损或人身伤亡的由该用户承担责任。",
"goodsList.submit": "确认并提交",
"houseKey.FriendsName": "亲友姓名",
"houseKey.AuthorizationDate": "授权期限",
"houseKey.ReceiveNotifications": "接收通知",
"houseKey.EnableNotifications": "打开接受通知",
"houseKey.PhoneNumber": "电话号码",
"houseKey.email": "邮箱",
"houseKey.EnterAuthorizedPhoneNumber": "填写授权方的手机号",
"houseKey.AddAuthorization": "新增授权",
"houseKey.EnterFriendsName": "填写亲友姓名",
"houseKey.EnterAuthorizationDate": "填写授权期限",
"houseKey.EnterPhoneNumber": "填写电话号码",
"houseKey.EnterEmail": "填写邮箱",
"houseKey.CannotAuthorizeYourself": "不能授权给自己",
"houseKey.UpdateSuccessful": "更新成功",
"houseKey.AddedSuccessfully": "新增成功",
"houseKey.date": "年-月-日",
"houseKey.overdue": "授权期限已过期,请重新选择授权期限并更新",
"houseKey.otherPhone": "填写授权方的手机号",
"houseKey.otherEmail": "填写授权方的邮箱",
"houseKey.getNote": "打开接受通知",
"unitTypeDetail.oneMonth": "租期:一个月起租",
"unitTypeDetail.reference": "参考",
"unitTypeDetail.discount": "优惠",
"reserve.FULLNAME": "用户姓名",
"reserve.PHONE": "手機號碼",
"reserve.REGION": "城市地区",
"reserve.TYPE": "所属类型",
"reserve.PHONE NUMBER": "手机号码",
"reserve.Individual & Family": "个人&家庭",
"reserve.Business & E-commerce": "企业&电商",
"reserve.Retail & Store": "零售&门店",
"reserve.contentTips": "预约成功,请耐心等待工作人员联系,保持手机畅通,感谢!",
"inviteDetail.title": "邀请记录",
"inviteDetail.Username": "用户名称",
"inviteDetail.Registration Date": "注册日期",
"inviteDetail.Status": "状态",
"inviteDetail.No invitation": "抱歉,暂无邀请记录,请分享邀请。",
"inviteDetail.SORRY": "抱歉,没有记录。请分享并邀请。",
"inviteDetail.Share Invitation": "分享邀请",
"referrerInfo.company": "中介公司",
"referrerInfo.branch": "中介分行",
"referrerInfo.commission": "中介费百分比",
"referrerInfo.inviteRegister": "邀请注册",
"referrerInfo.inviteRecord": "邀请记录",
"referrerInfo.inviteUserName": "用户",
"referrerInfo.invitePhone": "手机号",
"referrerInfo.registrationTime": "注册时间",
"referrerInfo.inviteEmpty": "暂无邀请记录",
"referrerInfo.loadQrCode": "下载二维码",
"referrerInfo.loadPoster": "下载海报",
"referrerInfo.forwardInvitation": "转发邀请",
"pointsMall.title": "积分商城",
"pointsMall.myPoints": "我的积分",
"pointsMall.pointsUnit": "积分",
"pointsMall.exchange": "兑换",
"pointsMall.exchangeFormTitle": "填写收货信息",
"pointsMall.submit": "提交",
"pointsMall.receiverName": "姓名",
"pointsMall.phone": "手机号",
"pointsMall.address": "地址",
"pointsMall.placeholderName": "请输入收货人姓名",
"pointsMall.placeholderPhone": "请输入手机号码",
"pointsMall.placeholderAddress": "请输入详细收货地址",
"pointsMall.exchangeConfirmTitle": "兑换确认",
"pointsMall.exchangeConfirmTip": "是否确认使用 {points} 积分兑换?",
"pointsMall.stock": "库存",
"pointsMall.successTitle": "领取成功",
"pointsMall.successTip": "兑换成功,请耐心等候工作人员联系!感谢!",
"pointsMall.toastOutOfStock": "库存不足",
"pointsMall.toastNotEnoughPoints": "积分不足",
"pointsMall.toastExchanging": "兑换中...",
"pointsMall.toastExchangeFailed": "兑换失败",
"pointsMall.exchangeRecordTitle": "兑换记录",
"pointsMall.noExchangeRecord": "暂无兑换记录"
}

24
locale/zh-Hant.json Normal file
View File

@ -0,0 +1,24 @@
{
"locale.auto": "系統",
"locale.en": "English",
"locale.zh-hans": "简体中文",
"locale.zh-hant": "繁體中文",
"locale.ja": "日语",
"index.title": "Hello i18n",
"index.home": "主頁",
"index.component": "組件",
"index.api": "API",
"index.schema": "Schema",
"index.demo": "uni-app 國際化演示",
"index.demo-description": "包含 uni-framework、manifest.json、pages.json、tabbar、頁面、組件、API、Schema",
"index.detail": "詳情",
"index.language": "語言",
"index.language-info": "語言信息",
"index.system-language": "系統語言",
"index.application-language": "應用語言",
"index.language-change-confirm": "應用此設置將重啟App",
"api.message": "提示",
"schema.name": "姓名",
"schema.add": "新增",
"schema.add-success": "新增成功"
}

20
main.js Normal file
View File

@ -0,0 +1,20 @@
import { createSSRApp } from "vue";
import App from "./App";
import i18n from "./locale/index";
import * as Pinia from "pinia";
// import VConsole from 'vconsole'
// new VConsole()
// import '@/uni.scss'
// 引入uvUI
import uvUI from "@/uni_modules/uv-ui-tools";
export function createApp() {
const app = createSSRApp(App);
app.use(uvUI);
app.use(Pinia.createPinia());
app.use(i18n);
return {
app,
Pinia,
};
}

113
manifest.json Normal file
View File

@ -0,0 +1,113 @@
{
"name" : "金刚迷你仓",
"appid" : "__UNI__FB6F2F3",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"optimization" : {
"subPackages" : true
},
"runmode" : "liberate", //
/* 5+App */
"usingComponents" : true,
"nvueCompiler" : "EliteSys",
"nvueStyleCompiler" : "EliteSys",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
/* */
"distribute" : {
/* */
"android" : {
/* android */
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
/* ios */
"sdkConfigs" : {}
}
},
/* SDK */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx3c4ab696101d77d1",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {
"scope.userLocation" : {
"desc" : "将获取你的具体位置信息,用于辅助显示最近店铺"
}
},
"plugins" : {
"player" : {
"version" : "2.0.0",
"provider" : "wxa75efa648b60994b"
}
},
"requiredPrivateInfos" : [ "getLocation" ]
},
"vueVersion" : "3",
"h5" : {
"router" : {
"base" : "./"
},
"devServer" : {
"port" : 8999,
"proxy" : {
"/api" : {
"target" : "http://localhost:5182",
"ws" : true,
"changeOrigin" : true
}
}
},
"optimization" : {
"treeShaking" : {
"enable" : true
}
},
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "B5ZBZ-S4SKW-YYQR5-3BVNP-NX4NQ-FYFYF"
}
}
}
},
"locale" : "en",
"fallbackLocale" : "en",
"mp-xhs" : {
"appid" : "6786436ac669e40001348567",
"permission" : {
"scope.userLocation" : {
"desc" : "将获取你的具体位置信息,用于辅助显示最近店铺"
}
}
}
}

1443
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "金刚迷你仓",
"version": "1.0.0",
"description": "金刚迷你仓",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "YOGO",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",
"vconsole": "^3.15.1",
"vue3-google-map": "^0.20.0"
},
"devDependencies": {
"sass-loader": "^10.5.2"
}
}

281
pages.json Normal file
View File

@ -0,0 +1,281 @@
{
"subPackages": [
{
"name": "pagesb",
"root": "pagesb",
"pages": [
{
"path": "changeUser/index"
},
{
"path": "referrerInfo/index"
},
{
"path": "pointsMall/index"
},
{
"path": "invitation/index"
},
{
"path": "houseKey/index"
},
{
"path": "flashSale/index"
},
{
"path": "latestEvents/index"
},
{
"path": "activityDetail/index",
"style": {
"navigationBarTitleText": " "
}
},
{
"path": "invoice/index"
},
{
"path": "invoiceApplyforRecord/index"
},
{
"path": "invoiceApply/index"
},
{
"path": "videoTutorial/index"
},
{
"path": "reserve/index"
},
{
"path": "validationInfo/index",
"style": {
"navigationBarTitleText": "实名验证"
}
},
{
"path": "userguide/index"
},
{
"path": "unittypeDetail/index"
},
{
"path": "initLock/index"
},
{
"path": "maskUser/index"
},
{
"path": "AControl/index"
}
],
"plugins": {
"ttPlugin": {
"version": "3.0.6",
"provider": "wx43d5971c94455481"
}
}
}
],
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"mp-weixin": {
"usingComponents": {
"player-component": "plugin://player/video"
}
}
}
},
{
"path": "pages/goodsList/index",
"style": {
"navigationBarTitleText": " "
}
},
{
"path": "pages/renewOrder/index",
"style": {
"navigationBarTitleText": " "
}
},
{
"path": "pages/webview/web",
"style": {
"navigationBarTitleText": " ",
"navigationStyle": "default"
}
},
{
"path": "pages/book/book",
"style": {
"navigationBarTitleText": "Book"
}
},
{
"path": "pages/facecode/facecode",
"style": {
"navigationBarTitleText": "facecode"
}
},
{
"path": "pages/book/index",
"style": {
"navigationBarTitleText": "Book"
}
},
{
"path": "pages/unlock/index",
"style": {
"navigationBarTitleText": "订单"
}
},
{
"path": "pages/personal/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/book/mapmode",
"style": {
"navigationBarTitleText": "Map"
}
},
{
"path": "pages/site/index",
"style": {
"navigationBarTitleText": "门店"
}
},
{
"path": "pages/setOrder/index",
"style": {
"navigationBarTitleText": "Order"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "Login"
}
},
{
"path": "pages/register/index",
"style": {
"navigationBarTitleText": "Register"
}
},
{
"path": "pages/forgotPawd/index",
"style": {
"navigationBarTitleText": "ForgotPAWd"
}
},
{
"path": "pages/book/navigate",
"style": {
"navigationBarTitleText": " ",
"navigationStyle": "default"
}
},
{
"path": "pages/orderdetail/index",
"style": {
"navigationBarTitleText": "Orderdetail"
}
},
{
"path": "pages/orderdetail/lock",
"style": {
"navigationBarTitleText": "Lock"
}
},
{
"path": "pages/orderdetail/door",
"style": {
"navigationBarTitleText": "Door"
}
},
{
"path": "pages/personal/customerAi",
"style": {
"navigationBarTitleText": " ",
"navigationStyle": "default"
}
},
{
"path": "pages/evaluate/index",
"style": {
"navigationBarTitleText": "Evaluate"
}
},
{
"path": "pages/call/index",
"style": {
"navigationBarTitleText": "Call"
}
},
{
"path": "pages/invite/index",
"style": {
"navigationBarTitleText": "Home"
}
}
],
"tabBar": {
"list": [
{
"pagePath": "pages/index/index",
"text": " ",
"iconPath": "static/tabbar/index.png",
"selectedIconPath": "static/tabbar/index.png"
},
{
"pagePath": "pages/book/index",
"text": " ",
"iconPath": "static/tabbar/book.png",
"selectedIconPath": "static/tabbar/book.png"
},
{
"pagePath": "pages/call/index",
"iconPath": "static/tabbar/call.png",
"selectedIconPath": "static/tabbar/call.png"
},
{
"pagePath": "pages/unlock/index",
"text": " ",
"iconPath": "static/tabbar/unlock.png",
"selectedIconPath": "static/tabbar/unlock.png"
},
{
"pagePath": "pages/personal/index",
"text": " ",
"iconPath": "static/tabbar/personal.png",
"selectedIconPath": "static/tabbar/personal.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "Elitesys",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom",
"app-plus": {
"background": "#efeff4"
}
},
"condition": {
//
"current": 0, //(list )
"list": [
{
"name": "", //
"path": "", //
"query": "" //onLoad
}
]
}
}

53
pages/book/book.vue Normal file
View File

@ -0,0 +1,53 @@
<template>
<view style="padding: 100px 0;display: flex;justify-content: center;height: 100%;align-items: center;">
<uv-button type="success" size="large" @click="goToSetOrder">跳转下单(Go to Order)</uv-button>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useLockApi } from "@/Apis/lock.js";
const api = useLockApi();
const state = ref({
lockerId: "",
});
onLoad((params) => {
if (params?.q) {
let urlParams = decodeURIComponent(params.q);
state.value.lockerId = urlParams.split("?id=")[1];
goToSetOrder()
}
});
const goToSetOrder= async()=>{
try {
uni.showLoading();
const {code,data} = await api.GetNewLockerId({oldLockerId: state.value.lockerId});
uni.hideLoading();
if (code === 200) {
const id = data || state.value.lockerId;
uni.navigateTo({
url: `/pages/setOrder/index?id=${id}`,
});
}else {
uni.showToast({
title: "数据出错",
icon: "none",
});
}
}catch (error) {
uni.showToast({
title: "数据出错",
icon: "none",
});
uni.hideLoading();
console.error("Error:", error);
}
};
</script>
<style>
</style>

456
pages/book/index.vue Normal file
View File

@ -0,0 +1,456 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]">
<view class="header-wrap">
<wxNavbar :title="$t('tabbar.book')"></wxNavbar>
<!-- 头部的筛选 -->
<view class="header">
<view class="header-text" @click="open">
<uv-icon name="dropdown" custom-prefix="custom-icon" size="16" :color="themeInfo.iconColor"></uv-icon>
&nbsp;&nbsp;{{ $t("book.location") }}:&nbsp;{{ popupData.selectCity }}
</view>
<view class="header-text" @click="tomapMode">
{{ $t("book.map") }}&nbsp;&nbsp;
<uv-icon name="setting1" custom-prefix="custom-icon" size="16" :color="themeInfo.iconColor"></uv-icon>
</view>
</view>
</view>
<!-- location窗口 -->
<uv-overlay :show="popupData.show" @click="open" z-index="99" >
<view class="location-popup">
<view class="select-area-wrap" :style="{ 'margin-top': `${state.navHeight}px` }" @click.stop.prevent>
<view class="city inner-wrap">
<view class="top-wrap">
<uv-icon name="halfArrow" custom-prefix="custom-icon" size="16" color="#0F2232"></uv-icon>
<text class="text">{{ $t("book.city") }}</text>
</view>
<view class="select-wrap">
<view
class="select-item"
v-for="(item, index) in popupData.cityData"
:key="index"
:class="{ select: item === popupData.selectCity }"
@click="handleCity(item)">
<view class="circle"></view>
<view class="name">{{ item }}</view>
</view>
</view>
</view>
<view class="border"></view>
<view class="area inner-wrap">
<view class="top-wrap">
<uv-icon name="halfArrow" custom-prefix="custom-icon" size="16" color="#0F2232"></uv-icon>
<text class="text">{{ $t("book.area") }}</text>
</view>
<view class="select-wrap">
<view
class="select-item"
v-for="(item, index) in popupData.areaData"
:key="index"
:class="{ select: item === popupData.selectDistrict }"
@click="handleDistrict(item)">
<view class="circle"></view>
<view class="name">{{ item }}</view>
</view>
</view>
</view>
</view>
</view>
</uv-overlay>
<!-- 热推门店详情 -->
<view class="get-location-wrap" @click="handleAuthorize" v-if="locationState.showGetLocation">
{{ $t("book.get") }}
</view>
<view :style="{ 'margin-top': `${state.navHeight}px` }" class="shopDetail" v-if="siteData.list?.length">
<siteDetail v-for="item in siteData.list" :key="item.id" :siteItem="item" @showCode="handleShowCode"></siteDetail>
</view>
<view class="footer">
<myCustomtTabBar direction="horizontal" :show-icon="true" :selected="2" @onTabItemTap="onTabItemTap" />
</view>
<!-- 门禁二维码 -->
<myPopup v-model="state.showQrcode" bgColor="transparent">
<view class="qrcode-wrap">
<view class="get-code-btn" v-show="!state.qrcodeUrl">
<uv-button @click="GetQRCode">
{{ $t("book.getCode") }}
</uv-button>
</view>
<image class="qrcodeImg" :src="state.qrcodeUrl"></image>
</view>
<view class="btn-wrap">
<uv-button @click="RemoteOpenDoor" :loading="state.openDoorLoading" :loadingText="t('unlock.remoteOpenLoading')">{{ state.openDoorText }}</uv-button>
</view>
</myPopup>
</view>
</template>
<script setup>
import { ref, reactive } from "vue";
import { onLoad, onShow, onHide, onShareAppMessage} from "@dcloudio/uni-app";
import { onTabItemTap, getDistance, navbarHeightAndStatusBarHeight,shareParam,mergeFiveGoatStores } from "@/utils/common.js";
import { ClientSite } from "@/Apis/book.js";
import wxNavbar from "@/components/wxNavbar.vue";
import myCustomtTabBar from "@/components/myCustomtTabBar.vue";
import siteDetail from '@/components/siteDetail.vue';
import myPopup from '@/components/myPopup.vue';
import { useLockApi } from '@/Apis/lock.js';
import { AppId } from '@/config/index.js'
//
import { useMainStore } from "@/store/index.js";
const { themeInfo, storeState } = useMainStore();
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import { useLocation } from "@/hooks/useLocation";
const { locationState, openLocationAuthorize, getLocation } = useLocation();
const getLockApi = useLockApi();
const getApi = ClientSite();
const state = reactive({
navHeight: 0,
showQrcode: false,
qrcodeUrl: '',
firstLoad: false,
openDoorLoading: false,
clickItem: {},
openDoorText: t('unlock.remoteOpen'),
});
onShareAppMessage((res) => {
if (res.from === "button") {
//
console.log(res.target);
}
return shareParam;
});
onLoad(() => {
uni.hideTabBar();
state.navHeight = Number(navbarHeightAndStatusBarHeight().navbarHeight) + 50;
//
getCityData();
state.firstLoad = true;
// onshow
// #ifdef MP-XHS
getLocation().finally(() => {
getSiteDetail();
// #ifndef MP-WEIXIN
locationState.showGetLocation = false;
// #endif
});
// #endif
});
onShow(() => {
getLocation().finally(() => {
getSiteDetail();
// #ifndef MP-WEIXIN
locationState.showGetLocation = false;
// #endif
});
});
onHide(() => {
state.firstLoad = false;
});
/**
* 开门功能相关
*/
const handleShowCode = (item) => {
state.clickItem = item;
state.showQrcode = true;
state.qrcodeUrl = '';
}
//
const RemoteOpenDoor = () => {
if (!state.clickItem?.id || state.openDoorLoading) return;
state.openDoorLoading = true;
getLockApi.RemoteOpenDoor({ siteId: state.clickItem.id }).then(res => {
state.openDoorLoading = false;
if (res.code === 200) {
uni.showToast({
title: t('unlock.remoteOpenSuccess'),
icon: 'none'
});
state.openDoorText = t('unlock.remoteOpenSuccess');
setTimeout(() => {
state.openDoorText = t('unlock.remoteOpen');
}, 3000);
} else {
uni.showToast({
title: t('unlock.remoteOpenFail'),
icon: 'none'
});
}
});
}
//
const GetQRCode = () => {
if (!state.clickItem?.id) return;
uni.showLoading();
getLockApi.GetAccesscontrolQRCodeBySite({ siteId: state.clickItem.id }).then(res => {
if (res.code === 200) {
state.qrcodeUrl = res.data;
}
uni.hideLoading();
});
}
//
const tomapMode = ()=>{
uni.navigateTo({
url: "/pages/book/mapmode"
});
}
/**
* 顶部popup 相关功能
*/
const popupData = reactive({
show: false,
selectCity: "",
selectDistrict: "",
cityData: [],
areaData: [],
});
const open = () => {
popupData.show = !popupData.show;
}
const getCityData = () => {
getApi.GetCityAll().then(res => {
if(res.code === 200) {
popupData.cityData = res.data;
popupData.cityData.unshift("全部");
//
popupData.selectCity = popupData.cityData[0];
}
})
}
const handleCity = (city) => {
popupData.selectCity = city;
if (city === "全部") {
popupData.areaData = ["全部"];
popupData.selectDistrict = "全部";
getSiteDetail();
} else {
getApi.GetDistrictByCity(city).then(res => {
if (res.code === 200) {
popupData.areaData = res.data;
popupData.areaData.unshift("全部");
popupData.selectDistrict = popupData.areaData[0];
}
let currentCity = city === "全部" ? "" : city;
getSiteDetail(currentCity, "");
});
}
}
const handleDistrict = (item) => {
popupData.selectDistrict = item;
let currentCity = popupData.selectCity === "全部" ? "" : popupData.selectCity;
let district = item === "全部" ? "" : item;
getSiteDetail(currentCity, district);
}
let siteData = reactive({
list: [],
isLoading: false,
});
//
const getSiteDetail = (city, district,isAll) => {
if (siteData.isLoading) return;
// #ifdef MP-WEIXIN
// if (locationState.showGetLocation) {
// uni.showToast({
// title: t("book.getSite"),
// icon: "none",
// duration: 2000
// });
// return;
// }
// #endif
uni.showLoading();
siteData.isLoading = true;
let getCity = city || "";
let getDistrict = district || "";
if (!city) {
getCity = popupData.selectCity == "全部" ? "" : popupData.selectCity;
getDistrict = popupData.selectDistrict == "全部" ? "" : popupData.selectDistrict;
}
if(isAll){
getCity = "";
getDistrict = "";
}
getApi.getSiteDetailsAll({
city: getCity,
district: getDistrict
}).then(res => {
if (res.code === 200) {
siteData.list = mergeFiveGoatStores(res.data);
if (locationState?.latitude && locationState?.longitude) {
const { latitude, longitude } = locationState;
siteData.list.forEach(item => {
const { distance, number } = getDistance(latitude, longitude, item.latitude, item.longitude);
item.distance = distance;
item.distanceNumber = number;
});
siteData.list.sort((a, b) => a.distanceNumber - b.distanceNumber);
if (state.firstLoad) filterSiteData();
}else {
//
if(AppId === 'wxb20921dfdd0b94f4' || AppId === 'wx3c4ab696101d77d1') {
if (state.firstLoad) filterSiteData(true);
}
}
}
uni.hideLoading();
siteData.isLoading = false;
}).catch(err => {
uni.hideLoading();
siteData.isLoading = false;
});
}
const handleAuthorize = () => {
openLocationAuthorize().then(res => {
state.firstLoad = true;
if (res) getSiteDetail('全部','全部',true);
});
}
// noLocation
const filterSiteData = (noLocation) => {
state.firstLoad = false;
if (!siteData.list.length) return;
let city = siteData.list[0]['city'];
// appid
if(noLocation){
if(AppId === 'wxb20921dfdd0b94f4' || AppId === 'wx3c4ab696101d77d1') {
city = siteData.list.find(item => item.city.indexOf("深圳") !== -1)?.city;
}
}
siteData.list = siteData.list.filter((item) => item.city.indexOf(city) !== -1);
popupData.selectCity = city;
getApi.GetDistrictByCity(city).then(res => {
if (res.code === 200) {
popupData.areaData = res.data;
popupData.areaData.unshift("全部");
popupData.selectDistrict = popupData.areaData[0];
}
});
}
</script>
<style lang="scss" scoped>
@import '@/static/style/theme.scss';
.container {
width: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
.qrcode-wrap {
position: relative;
width: 100%;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
border-radius: 18rpx;
.get-code-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.qrcodeImg {
background-color: #FFFFFF;
height: 600rpx;
width: 600rpx;
}
}
.btn-wrap {
width: 600rpx;
margin: 0 auto;
margin-top: 40rpx;
}
.qrcode-wrap,
.btn-wrap {
::v-deep .uv-button {
color: var(--text-color);
background-color: var(--btn-color1);
border: none;
font-size: 32rpx;
font-weight: bold;
text-justify: 20rpx;
.uv-button__loading-text {
font-size: 32rpx!important;
font-weight: bold;
text-justify: 20rpx;
}
}
}
}
.header-wrap {
position: fixed;
width: 100%;
background: linear-gradient(to bottom, var(--left-linear), var(--right-linear));
z-index: 999;
::v-deep .wxNavbar {
background: transparent !important;
}
}
//
.header {
height: 50px;
width: 100%;
padding: 0 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
.header-text {
font-size: 28rpx;
font-weight: bold;
display: flex;
align-items: center;
color: var(--whiteOrBlack);
& > .header-icon {
width: 28rpx;
height: 28rpx;
}
}
}
//
.shopDetail {
width: 100%;
margin-bottom: 300rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.get-location-wrap {
position: fixed;
height: 74rpx;
background-color: var(--main-color);
border-radius: 45rpx 0rpx 0rpx 45rpx;
border: 4rpx solid var(--stress-text);
box-shadow: 0rpx 4rpx 10rpx 0rpx rgba(0, 0, 0, 0.1);
right: -4px;
top: 75%;
padding: 0 20rpx 0 30rpx;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
color: var(--stress-text);
font-size: 28rpx;
font-weight: 700;
}
</style>

174
pages/book/map.vue Normal file
View File

@ -0,0 +1,174 @@
<template>
<div class="map" id="map">
<!-- #ifdef H5 -->
<!-- <GoogleMap
ref="googleMap"
api-key="AIzaSyC95SewUgAsDlcERNpJGxb845VoFGkAU2c"
style="width: 100%; height: 100%"
:center="center"
:zoom="15"
>
<CustomMarker v-for="(item,index) in props.markerList" :key='index' :options="{ position: item }">
<div class='markerBox' @click="markerClick(item,index)">
<div class="text">{{item.name}}</div>
<div class="circle"></div>
</div>
</CustomMarker>
</GoogleMap> -->
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<!-- <map style="width: 100%; height: 100%;" @markertap='markertap' :latitude="center.lat" :longitude="center.lng" :markers="WeixinMarkerList">
</map> -->
<!-- #endif -->
<map style="width: 750rpx; height: 100%;" :scale="3" @markertap='markertap' :latitude="center.lat" :include-points="includePoints" :show-location="true" :longitude="center.lng" :markers="WeixinMarkerList">
</map>
</div>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { wgs84ToGcj02 } from '@/utils/map.js'
import {
ref, watch,watchEffect
} from 'vue';
// #ifdef H5
import { GoogleMap,CustomMarker } from 'vue3-google-map';
// #endif
// onLoad(()=>{
// uni.hideTabBar()
// })
const includePoints = ref([]) //
const emit = defineEmits(["markerClick"])
const props = defineProps({
markerList:{
type:Array,
default:()=>[]
},
locationState:{
type:Object,
default:()=>{}
},
})
const markertap = (e)=>{
const event = JSON.parse(JSON.stringify(WeixinMarkerList.value[e.detail.markerId]||{}))
event.index = event.id
event.id = event.oId
center.value = {lat:event.latitude,lng:event.longitude}
currentMarker.value = event.index;
emit('markerClick',{event,index:event.index })
}
const WeixinMarkerList = ref([]);
const currentMarker = ref(0); // marker index
// watch(()=>props.markerList,()=>{
// WeixinMarkerList.value = props.markerList.map((item,index)=>{
// const [lng,lat] = wgs84ToGcj02(item.lng,item.lat)
// return {
// ...item,
// oId:item.id,
// id:index,
// latitude: lat,
// longitude: lng,
// iconPath:'/static/book/noSelectMapIcon.png'
// }
// })
// if(WeixinMarkerList.value.length){
// // center.value = {lat:WeixinMarkerList.value[0].latitude,lng:WeixinMarkerList.value[0].longitude}
// }
// includePoints.value = WeixinMarkerList.value.map((item)=>{
// return {
// latitude:item.latitude,
// longitude:item.longitude
// }
// })
// },{ deep: true })
watch(() => props.markerList, () => {
currentMarker.value = 0;
});
watchEffect(()=>{
WeixinMarkerList.value = props.markerList.map((item,index)=>{
// todo
// const [lng,lat] = wgs84ToGcj02(item.lng,item.lat)
return {
...item,
oId:item.id,
id:index,
latitude: item.lat,
width:20,
height:20,
longitude: item.lng,
iconPath: currentMarker.value == index ? '/static/book/selectMapIcon.png' : '/static/book/noSelectMapIcon.png'
}
});
includePoints.value = WeixinMarkerList.value.map((item)=>{
return {
latitude:item.latitude,
longitude:item.longitude
}
})
if(props.locationState.latitude && props.locationState.longitude){
center.value = {lat:props.locationState.latitude,lng:props.locationState.longitude}
includePoints.value.push({
latitude: center.value.lat,
longitude: center.value.lng
})
}
})
const googleMap = ref();
const marker = ref()
const selectMarker = ref()
const defaultCenter = { lat: 22.31615301, lng: 114.16999981 }; //
const center = ref(defaultCenter)
const markerClick=(event,index)=>{
center.value = event
emit('markerClick',{event,index})
}
</script>
<style lang="scss">
@import '@/static/style/theme.scss';
.map {
width: 100%;
height: 100%;
}
.markerBox{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #FFFFFF;
font-size: 26rpx;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
}
.text{
margin-bottom: 5rpx;
}
.circle{
width: 46rpx;
height: 46rpx;
border-radius: 99rpx;
border: 8rpx solid #00C8D5;
background-color: #005A6B;
}
.circle.click{
border-radius: 99rpx;
border: 8rpx solid #049EBB;
background-color: #00F6D4;
}
/* #ifdef MP-XHS */
xhs-map {
z-index: 1 !important;
}
/* #endif */
</style>

364
pages/book/mapmode.vue Normal file
View File

@ -0,0 +1,364 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]">
<view class="header-wrap">
<wxNavbar :title="$t('book.map')"></wxNavbar>
<!-- 头部的筛选 -->
<view class="header">
<view class="header-text" @click="open">
<uv-icon name="dropdown" custom-prefix="custom-icon" size="16" :color="themeInfo.iconColor"></uv-icon>
&nbsp;&nbsp;{{ $t("book.location") }}:&nbsp;{{ popupData.selectCity }}
</view>
<view class="header-text" @click="toList">
{{ $t("book.list") }}&nbsp;&nbsp;
<uv-icon name="setting1" custom-prefix="custom-icon" size="16" :color="themeInfo.iconColor"></uv-icon>
</view>
</view>
</view>
<!-- location窗口 -->
<uv-overlay :show="popupData.show" @click="open" z-index="99" >
<view class="location-popup">
<view class="select-area-wrap" :style="{ 'margin-top': `${state.navHeight}px` }" @click.stop.prevent>
<view class="city inner-wrap">
<view class="top-wrap">
<uv-icon name="halfArrow" custom-prefix="custom-icon" size="16" color="#0F2232"></uv-icon>
<text class="text">{{ $t("book.city") }}</text>
</view>
<view class="select-wrap">
<view
class="select-item"
v-for="(item, index) in popupData.cityData"
:key="index"
:class="{ select: item === popupData.selectCity }"
@click="handleCity(item)">
<view class="circle"></view>
<view class="name">{{ item }}</view>
</view>
</view>
</view>
<view class="border"></view>
<view class="area inner-wrap">
<view class="top-wrap">
<uv-icon name="halfArrow" custom-prefix="custom-icon" size="16" color="#0F2232"></uv-icon>
<text class="text">{{ $t("book.area") }}</text>
</view>
<view class="select-wrap">
<view
class="select-item"
v-for="(item, index) in popupData.areaData"
:key="index"
:class="{ select: item === popupData.selectDistrict }"
@click="handleDistrict(item)">
<view class="circle"></view>
<view class="name">{{ item }}</view>
</view>
</view>
</view>
</view>
</view>
</uv-overlay>
<!-- 地图 -->
<view class="mapBpx" :style="{ 'padding-top': `${state.navHeight}px` }" v-show="siteData.markerList">
<GoogleMap :markerList="siteData.markerList" :locationState="locationState" @markerClick="markerClick"></GoogleMap>
<view class="shopDetail" v-if="state.showMapDetail && siteData.selectSite?.id">
<view class="shop-detail">
<view class="close">
<uv-icon name="close" size="16" color="#969799" @click="closeMapDetail"></uv-icon>
</view>
<!-- 店铺图片 -->
<siteDetail :siteItem="siteData.selectSite"></siteDetail>
</view>
</view>
</view>
<view class="get-location-wrap" @click="handleAuthorize" v-if="locationState.showGetLocation">
{{ $t("book.get") }}
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import wxNavbar from "@/components/wxNavbar.vue";
import siteDetail from '@/components/siteDetail.vue';
import GoogleMap from "@/pages/book/map.vue";
import { ClientSite } from "/Apis/book.js";
import { getDistance, navbarHeightAndStatusBarHeight } from "@/utils/common.js";
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import { useLocation } from "@/hooks/useLocation";
const { locationState, openLocationAuthorize, getLocation } = useLocation();
const getApi = ClientSite();
const state = ref({
showMapDetail: true,
navHeight: 0,
firstLoad: false,
});
onMounted(() => {
state.firstLoad = true;
getCityData();
state.value.navHeight = Number(navbarHeightAndStatusBarHeight().navbarHeight) + 50;
getLocation().finally(() => {
getSiteDetail();
// #ifndef MP-WEIXIN
locationState.showGetLocation = false;
// #endif
});
});
const markerClick = (event) => {
siteData.selectSite = event.event;
// #ifdef MP-WEIXIN
if (locationState?.latitude && locationState?.longitude) {
let distanceData = getDistance(locationState.latitude, locationState.longitude, siteData.selectSite.latitude, siteData.selectSite.longitude);
siteData.selectSite.distance = distanceData.distance;
}
// #endif
state.value.showMapDetail = true;
};
const toList = () => {
uni.switchTab({
url: "/pages/book/index"
});
};
const closeMapDetail = () => {
state.value.showMapDetail = false;
};
const siteData = reactive({
list: [],
markerList: [],
selectSite: {},
isLoading: false,
});
const setMarkerList = () => {
//
let list = siteData.list.filter((item) => Number(item.latitude) && Number(item.longitude));
//
if (locationState?.latitude && locationState?.longitude) {
list.forEach(item => {
let distanceData = getDistance(locationState.latitude, locationState.longitude, item.latitude, item.longitude);
item.distance = distanceData.distance;
item.distanceNumber = distanceData.number;
});
list.sort((a, b) => a.distanceNumber - b.distanceNumber);
}
siteData.list = list;
if (state.firstLoad) {
filterSiteData();
} else {
siteData.markerList = list.map((item) => {
return {
...item,
lat: Number(item.latitude),
lng: Number(item.longitude)
};
});
siteData.selectSite = siteData.markerList[0] || {};
}
};
//
const getSiteDetail = (city, district) => {
if (siteData.isLoading) return;
// #ifdef MP-WEIXIN
// if (locationState.showGetLocation) {
// uni.showToast({
// title: t("book.getSite"),
// icon: "none",
// duration: 2000
// });
// return;
// }
// #endif
uni.showLoading();
state.isLoading = true;
getApi.getSiteDetailsAll({
city: city || "",
district: district || ""
}).then((res) => {
if (res.code === 200) {
siteData.list = res.data;
setMarkerList();
}
siteData.isLoading = false;
uni.hideLoading();
});
}
//
const filterSiteData = () => {
state.firstLoad = false;
if (!siteData.list.length) return;
let city = siteData.list[0]['city'];
siteData.list = siteData.list.filter((item) => item.city == city);
siteData.markerList = siteData.list.map((item) => {
return {
...item,
lat: Number(item.latitude),
lng: Number(item.longitude)
};
});
siteData.selectSite = siteData.markerList[0] || {};
popupData.selectCity = city;
getApi.GetDistrictByCity(city).then(res => {
if (res.code === 200) {
popupData.areaData = res.data;
popupData.areaData.unshift("全部");
popupData.selectDistrict = popupData.areaData[0];
}
});
}
/**
* 顶部popup 相关功能
*/
const popupData = reactive({
show: false,
selectCity: "",
selectDistrict: "",
cityData: [],
areaData: [],
});
const open = () => {
popupData.show = !popupData.show;
}
const getCityData = () => {
uni.showLoading();
getApi.GetCityAll().then(res => {
if(res.code === 200) {
popupData.cityData = res.data;
popupData.cityData.unshift("全部");
//
popupData.selectCity = popupData.cityData[0];
}
uni.hideLoading();
})
}
const handleCity = (city) => {
popupData.selectCity = city;
if (city === "全部") {
popupData.areaData = ["全部"];
popupData.selectDistrict = "全部";
getSiteDetail();
} else {
uni.showLoading();
getApi.GetDistrictByCity(city).then(res => {
if (res.code === 200) {
popupData.areaData = res.data;
popupData.areaData.unshift("全部");
popupData.selectDistrict = popupData.areaData[0];
}
uni.hideLoading();
let currentCity = city === "全部" ? "" : city;
getSiteDetail(currentCity, "");
});
}
}
const handleDistrict = (item) => {
popupData.selectDistrict = item;
let currentCity = popupData.selectCity === "全部" ? "" : popupData.selectCity;
let district = item === "全部" ? "" : item;
getSiteDetail(currentCity, district);
}
const handleAuthorize = () => {
openLocationAuthorize().then(res => {
if (res) getSiteDetail();
});
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
.header-wrap {
position: fixed;
width: 100%;
background: linear-gradient(to bottom, var(--left-linear), var(--right-linear));
z-index: 999;
::v-deep .wxNavbar {
background: transparent !important;
}
}
//
.header {
height: 50px;
width: 100%;
padding: 0 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
.header-text {
font-size: 28rpx;
font-weight: bold;
display: flex;
align-items: center;
color: var(--whiteOrBlack);
& > .header-icon {
width: 28rpx;
height: 28rpx;
}
}
}
.mapBpx {
width: 100%;
height: 100vh;
position: relative;
.shopDetail {
pointer-events: none; /* 使子元素对鼠标事件透明 */
position: absolute;
bottom: 0;
margin: 0;
padding: 30rpx 0;
background: #FFFFFF;
border-radius: 20px 20px 0 0;
z-index: 9;
.close {
position: absolute;
right: 28rpx;
top: 20rpx;
}
.shop-detail {
pointer-events: all;
padding-bottom: 10rpx;
flex-wrap: wrap;
:deep(.site-detail) {
box-shadow: none;
}
}
}
}
.get-location-wrap {
margin-top: 400rpx;
width: 100%;
text-align: center;
padding: 10px 0;
border-top: 1px solid #DDDDDD;
border-bottom: 1px solid #DDDDDD;
background-color: #FFFFFF;
}
}
</style>

88
pages/book/navigate.vue Normal file
View File

@ -0,0 +1,88 @@
<template>
<view class="container">
<!-- <nav-bar></nav-bar> -->
<rich-text :nodes="siteGuideData"></rich-text>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app'
import { ClientSite } from '/Apis/book.js'
import { projectInfo } from '@/config'
// import NavBar from '../../components/navBar.vue';
onLoad((option) => {
siteId.value = option.id
type.value = option.type
getData()
})
onShareAppMessage((res) => {
return {
title: `${projectInfo.miniName}`,
path: `/pages/book/navigate?id=${siteId.value}&type=${type.value}`
};
});
const getApi = ClientSite()
const siteId = ref(null)
const type = ref()
const siteGuideData = ref([])
const getData = ()=>{
// tpye 1 线 2
if(type.value==2){
getStopCar()
}else{
getSiteGuide()
}
}
function getStopCar() {
uni.showLoading()
getApi.GetSiteStopCarGuideById(siteId.value).then(res => {
uni.hideLoading()
if(res.code === 200) {
siteGuideData.value = res.data
}
}).catch(err => {
uni.hideLoading()
})
}
function getSiteGuide() {
uni.showLoading()
getApi.GetSiteGuideById(siteId.value).then(res => {
uni.hideLoading()
if(res.code === 200) {
siteGuideData.value = res.data
}
}).catch(err => {
uni.hideLoading()
})
}
</script>
<style scoped lang="scss">
@import '@/static/style/theme.scss';
.container {
margin: 0;
padding: 0;
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: start;
// background: linear-gradient(to bottom, #0A83B7, #00AFBD);
background-attachment: fixed;
}
.image {
width: 68rpx;
height: 68rpx;
margin-left: 40rpx;
margin-top: 30rpx;
}
</style>

12
pages/call/index.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<view class="container">
空白页面站位
</view>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

396
pages/evaluate/index.vue Normal file
View File

@ -0,0 +1,396 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<navBar class="navBar"></navBar>
<view class="container-form">
<view class="form">
<view class="topBox">
{{ $t('evaluate.customerEvaluation') }}
</view>
<view class="midBox">
<!-- Overall rating评分 -->
<view class="midBox-detail">
<view class="circle"></view>
<view class="rate">{{ $t("evaluate.overallRating") }}</view>
<uv-rate
class="uv-rate"
:count="5"
v-model="rate.overallRating"
:activeColor="themeInfo.activeColor"
inactiveColor="rgb(13, 32, 49)"
size="40rpx"
>
</uv-rate>
</view>
<!-- User experience -->
<view class="midBox-detail-1">
<view class="circle"></view>
<view class="rate">{{ $t("evaluate.userExperience") }}</view>
<view class="border"></view>
</view>
<!-- Hospitality评分 -->
<view class="midBox-detail-2">
<view class="line"></view>
<view class="rate">{{ $t("evaluate.Hospitality") }}</view>
<uv-rate
class="uv-rate"
:count="5"
v-model="rate.Hospitality"
:activeColor="themeInfo.activeColor"
inactiveColor="rgb(13, 32, 49)"
size="40rpx"
>
</uv-rate>
</view>
<!-- Cleanliness评分 -->
<view class="midBox-detail-2">
<view class="line"></view>
<view class="rate">{{ $t("evaluate.cleanliness") }}</view>
<uv-rate
class="uv-rate"
:count="5"
v-model="rate.cleanliness"
:activeColor="themeInfo.activeColor"
inactiveColor="rgb(13, 32, 49)"
size="40rpx"
>
</uv-rate>
</view>
<!-- Convenience评分 -->
<view class="midBox-detail-2">
<view class="line"></view>
<view class="rate">{{ $t("evaluate.convenience") }}</view>
<uv-rate
class="uv-rate"
:count="5"
v-model="rate.convenience"
:activeColor="themeInfo.activeColor"
inactiveColor="rgb(13, 32, 49)"
size="40rpx"
>
</uv-rate>
</view>
</view>
<view class="textareaBox">
<uv-textarea
v-model="rate.textarea"
height="140.8rpx"
:placeholder="$t('evaluate.tips')"
customStyle="background: transparent; border-radius: 16rpx; border:none;"
textStyle="font-size: 24rpx; color: rgb(15, 34, 50); font-weight: 400; line-height: 35rpx;"
>
</uv-textarea>
<view class="textareaBox-bottom">
<view class="anonymous">
<uv-checkbox-group v-model="rate.checkboxValue">
<uv-checkbox activeColor="#A1A1A1"
name="true"
shape="circle"
:label="$t('evaluate.anonymous')"
>
</uv-checkbox>
</uv-checkbox-group>
<!-- <text>{{ $t('evaluate.anonymous') }}</text> -->
</view>
<view class="add-pictures" >
<uv-upload
:fileList="fileList1"
name="6"
:maxCount="1"
width="102.4rpx"
height="91.84rpx"
customStyle="width: 102.4rpx;height: 92rpx; border-radius: 12rpx;display: flex; justify-content: center; align-items: center;"
@afterRead="afterRead"
:previewFullImage="false"
@delete="deletePic"
>
<!-- <view class="add-pictures" @click="addPicture">
<uv-image src="../../static/evaluate/image.png" width="42rpx" height="39rpx"></uv-image>
<br>
add pictures
</view> -->
<image style="width: 102.4rpx; height: 91.84rpx;" src="../../static/evaluate/addPic.png" ></image>
</uv-upload>
</view>
</view>
</view>
<button @click="submit" v-show="ifsubmit" class="submit">
{{ $t('common.submit') }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import navBar from '@/components/navBar.vue'
import { ref } from 'vue';
import { useOrderApi } from '@/Apis/order.js'
import { onLoad } from '@dcloudio/uni-app'
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
const getApi = useOrderApi()
const count = ref(5);
const rate = ref({
orderId: '',
siteId: '',
overallRating: '',
Hospitality: '',
cleanliness: '',
convenience: '',
imageUrl: '',
textarea: "",
checkboxValue: [],
})
onLoad((params) => {
rate.value.orderId = params.orderId
rate.value.siteId = params.siteId
})
// const overAll = ref(0);
// const Hospitality = ref(0);
// const cleanliness = ref(0);
// const convenience = ref(0);
const ifsubmit = ref(true);
//
const fileList1 = ref([]);
//
const afterRead = async (event) => {
let lists = [].concat(event.file);
let fileListLen = fileList1.value.length;
lists.forEach(async (item) => {
fileList1.value.push({
...item,
status: 'uploading',
message: '上传中'
});
const result = await UploaderImage(item.url);
fileList1.value[fileListLen] = {
...item,
status: 'success',
message: '',
url: result
};
rate.value.imageUrl = result
fileListLen++;
});
}
//
async function UploaderImage(url) {
let url1 = '';
try {
const res = await getApi.UploaderImage({ filePath: url });
// const jsonstr = JSON.parse(res);
url1 = res.data;
} catch (error) {
throw error; //
}
return url1;
}
//
const deletePic = (event) => {
fileList1.value.splice(event.index, 1);
rate.value.imageUrl=''
};
//
async function submit() {
uni.showLoading()
const res = await getApi.SubmitOrderEvaluate({
orderId: String(rate.value.orderId),
siteId: String(rate.value.siteId),
overallRating: String(rate.value.overallRating),
hospitality: String(rate.value.Hospitality),
cleanliness: String(rate.value.cleanliness),
convenience: String(rate.value.convenience),
remark: String(rate.value.textarea),
imageUrl: String(rate.value.imageUrl),
isAnonymity: rate.value.checkboxValue.length === 1
});
if(res.code === 200) {
uni.hideLoading()
ifsubmit.value = false
uni.showToast({
title: '提交成功',
icon: 'none',
duration: 2000
});
setTimeout(() => {
uni.reLaunch({
url:'/pages/unlock/index'
})
},2000)
}
}
</script>
<style lang="scss" scoped>
@import '@/static/style/theme.scss';
uni-page-body {
height: 100%;
background: linear-gradient(0.00deg, rgb(1, 169, 188), rgb(10, 132, 184));
overflow: auto;
}
.navBar {
position: relative;
}
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: start;
height: 100vh;
background: linear-gradient(0.00deg, var(--left-linear), var(--right-linear));
}
.container-form {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
background: transparent;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.form {
position: relative;
width: 690rpx;
height: 1000rpx;
background: #fff;
border-radius: 16rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
& > .topBox {
width: 100%;
height: 116rpx;
background: #F7F7F7;
text-align: center;
line-height: 116rpx;
border-radius: 16rpx 16rpx 0 0;
}
& > .midBox {
width: 504rpx;
background: #fff;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.midBox-detail {
margin-top: 54rpx;
display: flex;
justify-content: flex-start;
align-items: center;
& > .circle {
width: 17rpx;
height: 17rpx;
background: var(--active-color);
border-radius: 100%;
}
& > .rate {
width: 214rpx;
margin-left: 25rpx;
font-weight: 500;
font-size: 26rpx;
color: #0F2232;
}
& > .uv-rate {
margin-left: 26rpx;
}
}
.midBox-detail-1 {
margin-top: 34rpx;
@extend .midBox-detail;
& > .border {
margin-left: 26rpx;
background: #979797;
width: 233rpx;
height: 2rpx;
}
}
.midBox-detail-2 {
@extend .midBox-detail;
margin-top: 21rpx;
& > .line {
width: 17rpx;
height: 2rpx;
background: #979797;
}
}
}
& >.textareaBox {
margin-top: 44rpx;
width: 630rpx;
height: 294rpx;
background: #F2F2F2;
border-radius: 8rpx;
& > .textareaBox-bottom {
width: 100%;
display: flex;
align-items: flex-end;
justify-content: flex-start;
& >.anonymous {
margin-left: 39rpx;
width: 200rpx;
display: flex;
align-items: center;
}
& > .add-pictures {
margin-left: 250rpx;
width: 102.4rpx;
height: 92rpx;
// border: 1px dashed rgb(151, 151, 151);
border-radius: 12rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 12rpx;
font-weight: 500;
color: #989898;
z-index: 10;
}
}
}
& > .submit {
margin-top: 41rpx;
border-radius: 14rpx;
background: var(--active-color);
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.13);
width: 630rpx;
height: 90rpx;
color: var(--text-color);
font-size: 37rpx;
font-weight: 500;
}
&::before {
content: "";
position: absolute;
top: -7px;
left: 4px;
width: 100%;
height: 14px;
background: radial-gradient(var(--right-linear) 0px, var(--right-linear) 5px, transparent 5px, transparent);
background-size: 14px 14px;
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<view style="padding: 100px 0;display: flex;justify-content: center;">
<uv-button @click="goAuth">回到订单页,可对相关订单进行门禁授权</uv-button>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import { reactive } from "vue";
import { useOrderApi } from '@/Apis/order.js'
const getApi = useOrderApi()
const state = reactive({
orderId: "",
mac: "",
});
onLoad(() => {
state.mac=options.id;
if(options.q){
var query=decodeURIComponent(options.q);
console.log(query)
state.mac=query.split("id=")[1];
if(!state.mac){
state.mac=query.split("facecode/")[1];
}
init()
}
});
const init = () => {
uni.showLoading();
uni.$on('loginSuccess',function(data){
console.log('监听到事件来自 loginSuccess ,携带参数 msg 为:' + data.msg);
getOrder()
})
}
const getOrder = () => {
uni.showLoading()
getApi.GetOrderList().then(res=>{
uni.hideLoading();
state.value.orderList = []
if(res.code === 200){
state.orderId = res.data.find(item=>item.accessControl.includes(state.mac) &&item.orderStartStatus == 2 && item.refundLockerStatus === 0)?.orderId
if(state.orderId){
uni.navigateTo({
url:`/pagesb/AControl/index?id=${state.orderId}`
})
}
}
})
}
const goAuth = () => {
//
uni.switchTab({
url: `/pages/unlock/index`,
});
};
</script>
<style>
</style>

300
pages/forgotPawd/index.vue Normal file
View File

@ -0,0 +1,300 @@
<template>
<view class="forgot-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="container">
<view class="logoBox">
<image src="/static/logo.png" mode=""></image>
<text>EMPOWER YOUR SELF STORAGE</text>
</view>
<view class="formBox">
<view class="form">
<uv-form labelPosition="left" :model="state" labelWidth='160rpx' :rules="rules" ref='formRef'>
<uv-form-item :label="$t('login.account')" prop="username" borderBottom>
<uv-input v-model="state.username" :placeholder="$t('login.account')" border="none">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.password')" prop="password" borderBottom >
<uv-input v-model="state.password" :placeholder="$t('login.password')" :type='state.passwordVisible?"text":"password"' border="none">
<template #suffix>
<uv-icon @click="state.passwordVisible = !state.passwordVisible" :name="state.passwordVisible?'eye-off-outline':'eye'" size="18" ></uv-icon>
</template>
</uv-input>
</uv-form-item>
<uv-form-item labelWidth='300rpx' :label="$t('login.confirm')" prop="passwordTow" borderBottom >
<uv-input v-model="state.passwordTow" :placeholder="$t('login.password')" :type='state.passwordVisible?"text":"password"' border="none">
<template #suffix>
<uv-icon @click="state.passwordVisible = !state.passwordVisible" :name="state.passwordVisible?'eye-off-outline':'eye'" size="18" ></uv-icon>
</template>
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.code')" prop="code" borderBottom >
<uv-input v-model="state.code" :placeholder="$t('login.code')" border="none">
</uv-input>
<template #right>
<uv-button size="small" type="primary" @click="send">{{ count ? count+'S' : $t("login.send") }}</uv-button>
</template>
</uv-form-item>
<view>
<uv-button shape="circle" block type="primary" @click="onSubmit">{{ $t("login.change") }}</uv-button>
<view class="goLogin" @click="goLogin">
{{ $t("login.toLogin") }}
</view>
</view>
</uv-form>
</view>
<!-- <van-form class="form" @submit="onSubmit" ref='formRef'>
<van-cell-group inset>
<van-field v-model="state.username" name="account" label="account" placeholder="account"
:rules="[{ required: true, message: 'Please input' }]" />
<van-field v-model="state.password" type="password" name="password" label="password"
:right-icon="state.passwordVisible ? 'eye-o' : 'eye-off-o'" placeholder="password"
:rules="[{ required: true, message: 'Please input' }]" />
<van-field
v-model="state.passwordTow"
type="password"
name="passwordTow"
label="confirm password"
:right-icon="state.passwordVisible ? 'eye-o' : 'eye-off-o'"
placeholder="password"
:rules="[{ required: true, message: 'Please input' },{ validator: passwordIsSame, message: 'The password is different.' }]"
/>
<van-field v-model="state.code" name="code" label="code" placeholder="code"
:rules="[{ required: true, message: 'Please input' }]">
<template #button>
<van-button size="small" type="primary" @click="send">{{count? count+'S' : 'SEND'}}</van-button>
</template>
</van-field>
<div>
<van-button round block type="primary" native-type="submit">
change
</van-button>
<view class="goLogin" @click="goLogin">
LOGIN AN ACCOUNT
</view>
</div>
</van-cell-group>
</van-form> -->
</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue';
import {
onTabItemTap
} from '/utils/common.js'
import {
useLoginApi
} from '@/Apis/login.js';
import { useCountDown } from "@/hooks/index";
import { navigateBack } from '@/utils/common.js'
import navBar from '@/components/navBar.vue'
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
const formRef = ref()
const { count, countDown,cancelCout } = useCountDown();
const getApi = useLoginApi()
//
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const passwordIsSame = ()=> state.value.password === state.value.passwordTow
const state = ref({
codeImage: '',
username: '',
uuid: '',
code: '',
password: '',
passwordTow:'',
passwordVisible: false,
})
const rules = {
username:[{ required: true, message: t("login.input"), trigger: ['blur', 'change'] }],
password:[{ required: true, message: t("login.input"), trigger: ['blur', 'change']}],
passwordTow:[{ required: true, message: t("login.input"), trigger: ['blur', 'change']}, { validator: passwordIsSame, message: t("login.different"), trigger: ['blur', 'change']}],
code:[{ required: true, message: t("login.input"), trigger: ['blur', 'change']}],
}
const togglePasswordVisibility = () => {
state.value.passwordVisible = !state.value.passwordVisible
};
const goLogin = ()=>{
navigateBack('/pages/login/index')
}
const send = () => {
formRef.value.validateField(['username','password','passwordTow'],vaild=>{
if(!vaild.length){
countDown(30,()=>{
ForgotPassword()
})
}
})
}
const ForgotPassword = () => {
uni.showLoading()
getApi.ForgotPassword(`"${state.value.username}"`).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title:res.msg,
icon:'none'
})
}else{
cancelCout()
}
})
}
const EmailVerify = () => {
uni.showLoading()
getApi.EmailVerify({
emailAddress: state.value.username,
verifyCode: state.value.code,
}).then(res => {
uni.hideLoading()
if (res.code === 200) {
UpdateUserInfo()
} else {
}
})
}
const UpdateUserInfo = () => {
uni.showLoading()
getApi.UpdateUserInfo({
emailAddress: state.value.username,
password: state.value.password,
type: 0,
}).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title:res.msg,
icon:'none'
})
goLogin()
} else {
}
})
}
const onSubmit = (values) => {
formRef.value.validate().then(()=>{
EmailVerify()
})
};
</script>
<style scoped lang="scss">
@import '@/static/style/theme.scss';
.forgot-wrap {
background: linear-gradient(0deg, var(--left-linear) 0%, var(--right-linear) 94.004%);
height: 100vh;
}
.container {
padding: 20rpx;
font-size: 14px;
line-height: 24px;
min-width: 100%;
.logoBox {
margin-top: 6%;
padding: 40upx;
display: flex;
flex-direction: column;
color: #FFFFFF;
font-weight: 900;
font-size: 34rpx;
image {
margin-top: 20rpx;
width: 256rpx;
height: 63rpx;
}
}
.formBox {
position: fixed;
bottom: 0;
width: 100%;
left: 0;
padding: 0 20rpx;
.form {
background: #FFFFFF;
padding: 60rpx 20rpx;
border-radius: 24rpx 24rpx 0 0;
.goLogin {
text-align: center;
padding: 30rpx 0;
}
::v-deep .uv-form-item__body{
margin-bottom: 25rpx;
background-color: #F6F7F9;
overflow: visible;
padding: 10px 16px;
.uv-form-item__body__left{
width: auto;
min-width: 140rpx;
flex-wrap: nowrap;
color: #617986;
font-weight: 600;
white-space: nowrap;
display: flex;
align-items: center;
}
}
// ::v-deep .van-field{
// margin-bottom: 25rpx;
// background-color: #F6F7F9;
// overflow: visible;
// .van-field__label{
// width: auto;
// min-width: 120rpx;
// flex-wrap: nowrap;
// color: #617986;
// font-weight: 600;
// white-space: nowrap;
// display: flex;
// align-items: center;
// }
// .van-field__error-message{
// position: absolute;
// top: 40rpx;
// }
// .van-field__value{
// position: relative;
// }
// .van-field__control{
// color: #121819;
// font-weight: 600;
// }
// .van-field__button {
// .van-button {
// height: 50rpx;
// }
// }
// }
}
::v-deep .uv-button {
border-color: var(--btn-color5);
background-color: var(--btn-color5);
}
}
}
.code {
width: 140rpx;
height: 56rpx;
}
</style>

332
pages/goodsList/index.vue Normal file
View File

@ -0,0 +1,332 @@
<template>
<view class="invoice-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="content">
<view class="info">
<view class="i-header">
<view class="tabbox">
<view class="li">
{{ $t("common.goodsList") }}
</view>
</view>
<view class="tips">
{{ $t("goodsList.info") }}
</view>
<view> </view>
</view>
<view class="infobox">
<uv-checkbox-group v-model="state.goodsIds" shape="circle" activeColor="var(--main-color)">
<view class="selectbox" v-for="(item, index) in state.dataList" :key="index">
<view class="li-title" @click="hadelShowHide(index)">
<view class="left"> {{ item.name }}{{ $t("goodsList.multi") }}</view>
<view class="right"> {{ item.show?'-':'+' }} </view>
</view>
<template v-if="item.show&&item.tree.length>0">
<view class="li-box" v-for="(item2, index2) in item.tree" :key="index2">
<view class="li2-title">
<view class="text"> [ {{item2.name}} ] </view>
<view class="line"> </view>
</view>
<view class="radioBox" v-if="item2.tree.length>0">
<uv-checkbox
:customStyle="{ marginBottom: '8px',marginRight:'8px' }"
v-for="(item3, index3) in item2.tree"
:key="index"
:label="item3.name"
:name="item3.id"
></uv-checkbox>
</view>
</view>
</template>
</view>
</uv-checkbox-group>
<view class="selectbox" style="margin-bottom: 20rpx;">
<view class="li-title">
<view class="left"> 其他{{ $t("goodsList.note") }}</view>
<view class="right"> &nbsp; </view>
</view>
</view>
<view class="textareaBox">
<uv-textarea
v-model="state.goodsListRemark"
height="140.8rpx"
:placeholder="$t('goodsList.note')"
customStyle="background: transparent; border-radius: 16rpx; border:none;"
textStyle="font-size: 24rpx; color: rgb(15, 34, 50); font-weight: 400; line-height: 35rpx;"
>
</uv-textarea>
</view>
<view class="tipsInfo">
<view class="p">
{{ $t("goodsList.tip1") }}
</view>
<view class="p">{{ $t("goodsList.tip2") }}</view>
<view class="p">{{ $t("goodsList.tip3") }}</view>
<view class="p">{{ $t("goodsList.tip4") }}</view>
</view>
<button class="btn" @click="submit" :disabled="state.goodsListRemark==''&&state.goodsIds.length==0">
{{ $t('goodsList.submit') }}
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import navBar from "@/components/navBar.vue";
import { useGoodsApi } from "@/Apis/goodsList.js";
import { onLoad } from '@dcloudio/uni-app';
import { navigateBack } from '@/utils/common.js';
const getApi = useGoodsApi();
import { reactive } from "vue";
const state = reactive({
goodsIds: [],
orderId: "",
dataList: [],
goodsListRemark:"",
});
const hadelShowHide = (index) => {
state.dataList[index].show = !state.dataList[index].show;
};
const getDataList = ()=>{
uni.showLoading()
getApi.GetGoodsList().then(res=>{
if(res.code == 200){
getDetails();
state.dataList = res.data;
state.dataList.forEach(item=>{
item.show = true;
})
}
}).finally(()=>{
uni.hideLoading()
})
}
const submit = ()=>{
uni.showLoading()
getApi.SubmitGoodsList({
goodsIds:state.goodsIds,
orderId:state.orderId,
goodsListRemark:state.goodsListRemark
}).then(res=>{
if(res.code == 200){
uni.showToast({
title:res.msg,
icon:"none"
})
setTimeout(()=>{
navigateBack()
},500)
} else {
uni.showToast({
title:res.msg,
icon:"none"
})
}
})
}
const getDetails = ()=>{
uni.showLoading()
getApi.GetSubmitGoodsList(state.orderId).then(res=>{
if(res.code == 200){
state.goodsIds = res.data.filter(item=>!item.isRemark).map(item=>item.value);
state.goodsListRemark = res.data.find(item=>item.isRemark)?.value
}
}).finally(()=>{
uni.hideLoading()
})
}
onLoad ((option) => {
state.orderId = option.orderId;
getDataList();
})
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
.invoice-wrap {
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
padding-bottom: 20rpx;
background: linear-gradient(
to bottom,
var(--right-linear),
var(--left-linear2)
);
.content {
width: 100%;
.infobox {
width: 100%;
padding: 20rpx 40rpx;
background-color: #ffffff;
position: relative;
.selectbox {
width: 100%;
.li-title{
padding: 8rpx 40rpx;
color: var(--text-color);
border-radius: 999rpx;
font-weight: bold;
font-size: 28rpx;
background: var(--active-color);
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.li2-title{
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 20rpx;
.text{
color: var(--stress-text);
font-weight: bold;
font-size: 28rpx;
}
.line{
height: 1px;
flex: 1;
background: var(--stress-text);
margin-left: 20rpx;
}
}
.radioBox {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
}
.textareaBox {
margin-top: 20rpx;
width: 100%;
height: 220rpx;
background: #F2F2F2;
border-radius: 8rpx;
}
&::before {
content: "";
z-index: 9;
position: absolute;
bottom: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(
var(--left-linear2) 0px,
var(--left-linear2) 5px,
transparent 5px,
transparent
);
background-size: 14px 14px;
}
.tipsInfo{
padding: 40rpx 0rpx;
font-size: 24rpx;
font-weight: bold;
.p{
margin-top: 8rpx;
}
}
}
.btn {
margin-bottom: 40rpx;
border-radius: 2px;
background: var(--active-color);
height: 90rpx;
line-height: 90rpx;
color: var(--text-color);
font-size: 28rpx;
font-weight: bold;
}
}
.i-header {
position: relative;
font-weight: bold;
.tips {
padding: 0 40rpx;
font-size: 22rpx;
font-weight: bold;
}
&::before {
content: "";
position: absolute;
top: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(
var(--right-linear) 0px,
var(--right-linear) 5px,
transparent 5px,
transparent
);
background-size: 14px 14px;
z-index: 9;
}
padding: 30upx 0;
width: 100%;
background-color: #f7f7f7;
color: #242e42;
.tabbox {
position: relative;
padding: 20upx 10%;
display: flex;
justify-content: space-around;
.li {
width: 50%;
background-color: transparent;
font-size: 32upx;
outline: none;
text-align: center;
}
.li.active {
font-weight: bold;
}
.bottom-line {
position: absolute;
left: calc(50% - 220rpx);
bottom: 0;
width: 160rpx;
height: 2.5px;
border-radius: 20px;
background: var(--main-color);
transition: left 0.5s ease;
&.right {
left: calc(50% + 60rpx);
}
}
}
}
}
</style>

1549
pages/index/index.vue Normal file

File diff suppressed because it is too large Load Diff

157
pages/invite/index.vue Normal file
View File

@ -0,0 +1,157 @@
<template>
<view class="invite-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="top-wrap">
<view class="text">{{ $t("invite.title1") }} <br> {{ $t("invite.title2") }}</view>
<uv-icon class="right-icon" name="present" custom-prefix="custom-icon" size="30" color="#FFFFFF"></uv-icon>
</view>
<view class="activity-wrap" v-for="item in state.activityList" :key="item.id">
<view class="top">
<view class="name-wrap">
<view class="dot"></view>
<view class="name">{{ item.name }}</view>
</view>
<view class="right">{{ $t("invite.number") }} 0/2</view>
</view>
<view class="content">
<view class="item">
<view class="label">{{ $t("invite.activity") }}: {{ item.mechanism }}</view>
<view class="label">{{ $t("invite.branch") }}: {{ item.branch }}</view>
<view class="label">{{ $t("invite.details") }}: {{ item.details }}</view>
</view>
</view>
<view class="bottom">
<view class="invite-btn">{{ $t("invite.toInvite") }}</view>
<view class="record">{{ $t("invite.record") }}>></view>
</view>
</view>
<view class="disclaimer1">{{ $t("invite.disclaimer") }}</view>
<view class="disclaimer2">{{ $t("invite.disContent") }}</view>
<image class="bottom-bg" src="@/static/personal/bottom-bg.png" mode="widthFix"></image>
</view>
</template>
<script setup>
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import navBar from '@/components/navBar.vue';
import { reactive } from "vue";
const state = reactive({
activityList: [
{ id: 33, name: "活动名称1", mechanism: "WInvite X friends to follow and register official account to get promotion coupon for storage rental.", branch: "ALL shops", details: 28008800, number: 1 },
{ id:2, name: "活动名称2", mechanism: "", branch: "ALL shops", details: 28008800, number: 1 },
]
})
</script>
<style lang="scss" scoped>
.invite-wrap {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
background: linear-gradient(to bottom, var(--right-linear), var(--left-linear));
.top-wrap {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 40rpx;
color: #FFFFFF;
border-radius: 20rpx;
padding: 30rpx 40rpx;
background: linear-gradient(to bottom, var(--left-linear), var(--right-linear));
.text {
font-size: 40rpx;
font-weight: bold;
margin-right: 60rpx;
}
.right-icon {
margin: 0 20rpx;
}
}
.activity-wrap {
width: 100%;
border-radius: 16rpx;
margin-top: 40rpx;
background-color: #FFFFFF;
overflow: hidden;
.top {
padding: 0 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #F6F6F6;
height: 80rpx;
.name-wrap {
display: flex;
align-items: center;
justify-content: space-between;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 18rpx;
background: var(--active-color);
}
}
.right {
font-size: 26rpx;
}
}
.content {
padding: 20rpx 20rpx 20rpx 50rpx;
font-size: 30rpx;
}
.bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 20rpx 20rpx 50rpx;
.invite-btn {
color: var(--text-color);
border-radius: 30rpx;
padding: 8rpx 30rpx;
font-size: 26rpx;
background: var(--active-color);
}
.record {
font-size: 24rpx;
}
}
}
.bottom-bg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}
.disclaimer1,
.disclaimer2 {
font-size: 28rpx ;
align-self: flex-start;
color: var(--text-color);
}
.disclaimer1 {
margin-top: 40rpx;
}
.disclaimer2 {
margin-left: 20rpx;
padding-bottom: 60px;
}
}
</style>

642
pages/login/index.vue Normal file
View File

@ -0,0 +1,642 @@
<template>
<view class="login-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="container">
<view class="logoBox">
<image src="/static/logo.png" mode=""></image>
<text>EMPOWER YOUR SELF STORAGE</text>
</view>
<view class="formBox">
<view class="form">
<uv-form labelPosition="left" :model="state" labelWidth="160rpx" :rules="rules" ref="formRef">
<!-- #ifndef MP-WEIXIN-->
<!-- #ifndef APP-PLUS -->
<uv-form-item :label="$t('login.account')" prop="username" borderBottom>
<uv-input v-model="state.username" :placeholder="$t('login.account')" border="none">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.password')" prop="password" borderBottom>
<uv-input v-model="state.password" :placeholder="$t('login.password')"
:type="state.passwordVisible ? 'text' : 'password'" border="none">
<template #suffix>
<uv-icon @click="
state.passwordVisible =
!state.passwordVisible
" :name="state.passwordVisible
? 'eye-off-outline'
: 'eye'
" size="18"></uv-icon>
</template>
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.code')" prop="code" borderBottom>
<uv-input v-model="state.code" :placeholder="$t('login.code')" border="none">
</uv-input>
<template #right>
<img class="code" @click="getCodeImage()" :src="'data:image/gif;base64,' + state.codeImage
" />
</template>
</uv-form-item>
<view class="btn-wrap">
<uv-button shape="circle" block type="primary" @click="onSubmit">
{{ $t('login.login') }}
</uv-button>
<view class="goLogin" @click="goRegister">
{{ $t("login.register") }}
</view>
<text class="forget-text" @click="goForgotPawd">{{ $t("login.forget") }}</text>
</view>
<!-- #endif -->
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<uv-form labelPosition="left" :model="state" labelWidth="160rpx" :rules="rulesApp" ref="formRef">
<uv-form-item :label="$t('login.phone')" prop="phone" borderBottom>
<uv-input
v-model="state.phone"
:placeholder="$t('login.phone')"
border="none"
style="flex: 1;"
>
<template v-slot:prefix>
<view @click="showAreaCodePicker" style="padding-right: 16rpx; min-width: 100rpx; color: #333;display: flex;align-items: center;">
{{ state.areaCode }} <view style="transform: rotate(90deg);"><uv-icon :size="14" name="play-right-fill"></uv-icon></view>
</view>
</template>
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.code')" prop="smsCode" borderBottom>
<uv-input v-model="state.smsCode" :placeholder="$t('login.inputCode')" border="none" />
<template #right>
<uv-button size="small" type="primary" :disabled="countdown > 0" @click="sendSmsCode">
{{ countdown > 0 ? `${countdown}s` : $t('login.getCode') }}
</uv-button>
</template>
</uv-form-item>
<view class="btn-wrap">
<uv-button shape="circle" block type="primary" @click="onAppLogin">
{{ $t('login.login') }}
</uv-button>
<view class="UserAgreementtips" :class="{'shake UserAgreementtips': state.isShaking}" @click="changeCheck(false)">
<uv-checkbox-group v-model="state.checked" @change="changeCheck(true)">
<uv-checkbox class="myCheckbox" :name="true"> </uv-checkbox>
</uv-checkbox-group> {{ $t('common.FirstTimeLoginTips') }},{{ $t('login.andAgreeTo') }}<text @click.stop="goAgreement">{{ $t('login.UserAgreement') }}</text>
</view>
</view>
</uv-form>
<uv-picker
ref="AreaCodePickerRef"
:columns="[areaCodeList]"
:confirmText="$t('common.confirm')"
:cancelText="$t('common.cancel')"
keyName="label"
@confirm="onAreaCodeConfirm"
/>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<uv-button customStyle="margin:100rpx 0" color="#5BBC6B" shape="circle" block type="primary"
@click="wxGetUserProfile">
{{ $t("login.wxLogin") }}
</uv-button>
<!-- #endif -->
</uv-form>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<uv-popup ref="popup"
customStyle="width: 80%; padding:20rpx 0; border-radius: 32rpx; display: flex; flex-direction: column; justify-content: center; align-items: center;"
@change="popupChange">
<text>{{ $t("common.bindPhone") }}</text>
<uv-form labelPosition="left" labelWidth="80" ref="formRef">
<uv-form-item :label="$t('common.userName')" prop="username" borderBottom>
<uv-input v-model="state.username" class="weui-input" border="none"
:placeholder="$t('common.userName')" />
</uv-form-item>
</uv-form>
<text style="padding: 0 40rpx; margin-top: 20rpx; text-align: center;">{{ $t('common.bindPhoneAfter') }}</text>
<button style="width: 80%; margin-top: 20px; background: #5BBC6B; color: #FFFFFF; line-height: 80rpx;"
open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">{{ $t("common.QuickBind") }}</button>
<view style="width: 80%; margin-top: 20px; text-align: center;" @click="isToHome">{{ $t("common.cancelBind") }}</view>
</uv-popup>
<!-- #endif -->
</view>
<myPopup
v-model="state.showAgreement"
type="center"
:mask-closable="false"
style="width: 100%; max-height: 80vh;padding:10px;"
>
<scroll-view scroll-y style="max-height: 60vh; padding: 16rpx; white-space: pre-line;">
<view>
<view class="AgreementTitle">用户协议 User Agreement</view>
<view>
<view> 1. 您同意遵守本应用的各项使用规定不得利用本应用进行违法或侵权行为 </view>
<view> You agree to comply with all the rules of this app and shall not use it for illegal or infringing activities.</view>
<view>2. 本应用有权根据需要修改或更新本协议内容修改后的协议将在应用内公告 </view>
<view> The app reserves the right to modify or update this agreement as needed, with changes announced within the app.</view>
<view>3. 您理解并同意首次登录即视为同意本协议及隐私政策 </view>
<view> You understand and agree that the first login signifies your acceptance of this User Agreement and Privacy Policy.</view>
</view>
</view>
<view style="margin-top: 24rpx;">
<view class="AgreementTitle">隐私政策 Privacy Policy</view>
<view>1. 我们重视您的隐私保护严格按照相关法律法规收集和使用您的个人信息 </view>
<view> We value your privacy and collect and use your personal information in accordance with relevant laws and regulations.</view>
<view>2. 您的手机号设备信息登录状态等信息将用于账户认证和服务优化 </view>
<view>Your phone number, device information, login status, and other data will be used for account authentication and service optimization.</view>
<view>3. 您的信息不会未经授权向第三方披露除非法律法规另有规定 </view>
<view> Your information will not be disclosed to third parties without your authorization, except as required by law.</view>
<view>4. 您可以通过应用设置管理您的个人信息权限 </view>
<view>You can manage your personal information permissions through the app settings. </view>
</view>
</scroll-view>
<view style="text-align: center; margin-top: 24rpx;">
<button
type="primary"
style="width: 80%; margin: 0 auto;"
@click="agree"
>
同意
</button>
</view>
</myPopup>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onTabItemTap, navigateBack, isToHome } from "/utils/common.js";
import { useLoginApi } from "@/Apis/login.js";
import { onLoad, onShow } from "@dcloudio/uni-app";
import navBar from "@/components/navBar.vue";
import myPopup from '@/components/myPopup.vue';
//
import { useMainStore } from "@/store/index.js";
const { themeInfo, storeState, getUserInfo } = useMainStore();
//
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const AreaCodePickerRef = ref();
const areaCodeList = ref([
{ label: '+86 (大陆)', value: '+86' },
{ label: '+852 (香港)', value: '+852' },
{ label: '+853 (澳门)', value: '+853' },
{ label: '+886 (台湾)', value: '+886' },
{ label: '+1 (美国/加拿大)', value: '+1' },
{ label: '+44 (英国)', value: '+44' },
{ label: '+81 (日本)', value: '+81' },
]);
const showAreaCodePicker = ()=>{
console.log(AreaCodePickerRef,"AreaCodePickerRef")
AreaCodePickerRef.value.open();
}
const closeAreaCodePicker = ()=>{
AreaCodePickerRef.value.close();
}
const ifMiniProgram = ref(false);
const getApi = useLoginApi();
const formRef = ref();
const rules = {
phone: [
{
required: true,
message: t("login.inputPhone"),
trigger: ["blur", "change"],
},
],
smsCode: [
{
required: true,
message: t("login.inputCode"),
trigger: ["blur", "change"],
},
],
username: [
{
required: true,
message: t("login.input"),
trigger: ["blur", "change"],
},
],
password: [
{
required: true,
message: t("login.input"),
trigger: ["blur", "change"],
},
],
code: [
{
required: true,
message: t("login.input"),
trigger: ["blur", "change"],
},
],
};
const goAgreement = ()=>{
state.value.showAgreement = true;
}
const onAreaCodeConfirm = (e) => {
state.value.areaCode = e.value[0].value;
closeAreaCodePicker();
};
const countdown = ref(0);
let timer = null;
const state = ref({
areaCode: '+86',
phone: '',
smsCode: '',
codeImage: "",
username: "",
uuid: "",
password: "",
passwordVisible: false,
code: "",
isShaking: false,
showAgreement:false, //
checked: [], // ,
looked: false, //
});
const changeCheck = (isChange)=>{
if(!state.value.looked){
if(isChange){
state.value.checked = !state.value.checked.length ? [true] : [];
}
uni.showToast({ title: t("common.checkAgreementUrl"), icon: "none" });
return
}
if(!isChange){
state.value.checked = !state.value.checked.length ? [true] : [];
}
}
const agree = ()=>{
state.value.looked = true;
state.value.checked = [true];
state.value.showAgreement = false;
}
const sendSmsCode = async () => {
if (!state.value.phone.trim()) {
uni.showToast({ title: t("login.phoneFormat"), icon: "none" });
return;
}
uni.showLoading({ title: t("login.sending") });
try {
const res = await getApi.sendSmsCode({ phone: state.value.phone });
if (res.code === 200) {
uni.showToast({ title: t("login.sendSuccess"), icon: "success" });
countdown.value = 60;
timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) clearInterval(timer);
}, 1000);
} else {
uni.showToast({ title: res.msg || "error", icon: "none" });
}
} finally {
uni.hideLoading();
}
};
const shake = () => {
state.value.isShaking = true;
setTimeout(() => {
state.value.isShaking = false;
}, 1000);
}
const onAppLogin = () => {
if(!state.value.checked.length){
shake();
return
}
// formRef.value
// .validate()
// .then(() => {
// uni.showLoading();
// getApi.appLogin({
// phone: state.value.phone,
// code: state.value.smsCode,
// }).then((res) => {
// uni.hideLoading();
// if (res.code === 200) {
// uni.setStorageSync("token", res.data.token);
// isToHome();
// } else {
// uni.showToast({ title: res.msg || "", icon: "none" });
// }
// });
// })
// .catch((error) => {
// console.log(error);
// });
};
const goRegister = () => {
uni.navigateTo({
url: "/pages/register/index",
});
};
const goForgotPawd = () => {
uni.navigateTo({
url: "/pages/forgotPawd/index",
});
};
const togglePasswordVisibility = () => {
state.value.passwordVisible = !state.value.passwordVisible;
};
//
const popupChange = (event) => {
//
if (!event.show) {
isToHome()
}
}
const getCodeImage = () => {
getApi.getCode().then((res) => {
if (res.code === 200) {
state.value.uuid = res.data.uuid;
state.value.codeImage = res.data.img;
}
});
};
// #ifndef MP-WEIXIN
getCodeImage();
// #endif
const onSubmit = (values) => {
const data = {
emailAddress: state.value.username,
password: state.value.password,
uuid: state.value.uuid,
code: state.value.code,
};
formRef.value
.validate()
.then((res) => {
uni.showLoading();
getApi.Login(data).then((res) => {
uni.hideLoading();
if (res.code == 200) {
uni.setStorageSync("token", res.data);
isToHome()
} else {
getCodeImage();
}
});
})
.catch((error) => {
console.log(error);
});
};
const code = ref("");
const encryptedData = ref("");
const iv = ref("");
const popup = ref()
const wxLogin = () => {
// code
return new Promise(function (reslove, reject) {
wx.login({
success(res) {
code.value = res.code;
reslove(res.code);
},
fail: (err) => {
reject(err)
console.error("wx.login调用失败", err);
},
})
})
}
const AuthorizedLogin = (data) => {
uni.showLoading()
getApi.AuthorizedLogin(data)
.then(async (res) => {
uni.hideLoading()
if (res.code == 200) {
storeState.token = res.data.token;
uni.setStorageSync("token", res.data.token);
uni.setStorageSync("openId", res.data.openId);
uni.removeStorage({key:'Pre_ID'})
uni.removeStorage({key:'mediatorId'})
const { data: userInfo } = await getUserInfo()
if (!userInfo.phone) {
popup.value.open()
} else {
isToHome()
}
}
});
}
async function wxGetUserProfile() {
uni.showLoading()
wx.getUserProfile({
desc: "用于完善会员资料", //
success: async (res) => {
const userInfo = res;
encryptedData.value = res.encryptedData;
iv.value = res.iv;
// code
uni.checkSession({
success: (res) => {
//
AuthorizedLogin({
code: code.value,
encryptedData: encryptedData.value,
iv: iv.value,
openId: uni.getStorageSync("openId"),
Pre_ID: uni.getStorageSync("Pre_ID"),
mediatorId: uni.getStorageSync('mediatorId')
})
},
fail: (err) => {
wxLogin().then(res=>{
AuthorizedLogin({
code: code.value,
encryptedData: encryptedData.value,
iv: iv.value,
openId: uni.getStorageSync("openId"),
Pre_ID: uni.getStorageSync("Pre_ID"),
mediatorId: uni.getStorageSync('mediatorId')
})
})
}
})
},
fail: (err) => {
uni.hideLoading()
console.error("获取用户信息失败:", err);
},
});
}
// token
async function wxChartLogin() {
await wxGetUserProfile();
}
const phoneNumber = ref('');
async function getPhoneNumber(e) {
uni.showLoading()
phoneNumber.value = e.detail.code;
if (e.detail.code) {
getApi.GetPhoneNumber({code:e.detail.code,userName:state.value.username.trim()}).then(res => {
if (res.code == 200) {
uni.hideLoading()
isToHome()
}
})
} else {
uni.hideLoading()
uni.showToast({
title: '获取手机号失败',
icon: 'none',
duration: 2000
});
}
}
onLoad(() => {
if (uni.getSystemInfoSync().hostName === "WeChat") {
wxLogin();
}
});
</script>
<!-- <style>
page {
background: linear-gradient(
0deg,
rgb(1, 169, 188) 0%,
rgb(10, 132, 184) 94.004%
);
min-height: 100%;
}
</style> -->
<style scoped lang="scss">
@import '@/static/style/theme.scss';
// uni-page-body {
// background: linear-gradient(
// 0deg,
// rgb(1, 169, 188) 0%,
// rgb(10, 132, 184) 94.004%
// );
// min-height: 100%;
// }
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-10px); }
40%, 80% { transform: translateX(10px); }
}
.shake {
animation: shake 0.8s ease-in-out;
}
.AgreementTitle{
font-weight: bold;
font-size: 32rpx;
margin-bottom: 12rpx;
}
.UserAgreementtips{
margin-top:20rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
text{
color: var(--active-color);
}
}
.login-wrap {
background: linear-gradient(0deg, var(--left-linear) 0%, var(--right-linear) 94.004%);
height: 100vh;
}
.container {
padding: 20rpx;
font-size: 14px;
line-height: 24px;
min-width: 100%;
.logoBox {
margin-top: 6%;
padding: 40upx;
display: flex;
flex-direction: column;
color: #ffffff;
font-weight: 900;
font-size: 34rpx;
image {
margin-top: 20rpx;
width: 256rpx;
height: 63rpx;
}
}
.formBox {
position: fixed;
bottom: 0;
width: 100%;
left: 0;
padding: 0 20rpx;
.form {
background: #ffffff;
padding: 60rpx 20rpx;
border-radius: 24rpx 24rpx 0 0;
.goLogin {
text-align: center;
padding: 30rpx 0;
}
.forget-text {
color: var(--btn-color2);
}
::v-deep .uv-form-item__body {
margin-bottom: 25rpx;
background-color: #f6f7f9;
overflow: visible;
padding: 10px 16px;
.uv-form-item__body__left {
width: auto;
min-width: 140rpx;
flex-wrap: nowrap;
color: #617986;
font-weight: 600;
white-space: nowrap;
display: flex;
align-items: center;
}
}
.btn-wrap {
::v-deep .uv-button {
border-color: var(--btn-color5);
background-color: var(--btn-color5);
}
}
}
}
}
.code {
width: 140rpx;
height: 56rpx;
}
</style>

224
pages/orderdetail/door.vue Normal file
View File

@ -0,0 +1,224 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="box">
<view class="top-box">
<view class="number">
{{ lockDetail.lockerName }}
</view>
<view class="detail">
{{ $t("detail.store") }} {{ lockDetail.siteName }} <br>
{{ $t("detail.unit") }} {{ lockDetail.unitTypeName }}<br>
<view v-if="lockDetail.orderStartStatus == 2 && lockDetail.refundLockerStatus === 0">{{ $t("detail.remain", { days: lockDetail.remainingDays }) }}</view>
<view style="color: red;" v-else>
{{ $t('common.status') }}
<template v-if="lockDetail.refundLockerStatus === 0">{{ $t("common.notStarted") }}</template>
<template v-else-if="lockDetail.refundLockerStatus === 1">{{ $t("unlock.cancelPending") }}</template>
<template v-else-if="lockDetail.refundLockerStatus === 2">{{ $t("unlock.outComplete") }}</template>
<template v-else-if="lockDetail.refundLockerStatus === 3">{{ $t("unlock.disapproval") }}</template>
</view>
</view>
</view>
<view class="paw" v-show="ifShow === false">
<uv-toast ref="toast"></uv-toast>
</view>
<img v-show="ifShow" class="paw" width="600rpx" height="600rpx" :src="QRCode" @click="ClickToZoomIn"></img>
<view class="tip">
{{ $t("door.tip") }}<br>
{{ $t("door.valid") }}
</view>
<button class="refresh" @click="GetQRCode">
<uv-icon name="reload" size="24" :color="themeInfo.iconColor"></uv-icon>
{{ $t("door.refresh") }}
</button>
<button class="refresh" @click="ClickToZoomIn">
<uv-icon name="search" size="24" :color="themeInfo.iconColor"></uv-icon>
{{ $t("common.ClickToZoomIn") }}
</button>
<view style="text-align: center;color: #FFFFFF;padding: 10rpx 0;">{{ $t("common.tryZooming") }}</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue';
import navBar from '@/components/navBar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { useOrderApi } from '@/Apis/order.js'
import { useLockApi } from '@/Apis/lock.js'
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
const getApi = useOrderApi()
const getLockApi = useLockApi()
const orderId = ref()
const siteId = ref()
const lockDetail = ref([])
const QRCode = ref([])
const ifShow = ref(false)
const toast = ref()
onLoad((params) => {
// uni.showLoading({mask:true})
orderId.value = params.id
siteId.value = params.siteId
GetOrderDetail()
})
const ClickToZoomIn = () => {
uni.previewImage({
urls: [QRCode.value],
longPressActions: {
itemList: ['发送给朋友', '保存图片'],
}
})
}
async function GetOrderDetail() {
uni.showLoading({mask:true})
await getApi.GetOrderById({ orderId: orderId.value }).then(res=>{
lockDetail.value = res.data
GetQRCode()
})
}
//
function GetQRCode() {
uni.showLoading({mask:true})
getLockApi.GetAccesscontrolQRCodeBySite({siteId: siteId.value, orderId: orderId.value}).then(res=>{
if(res.code === 200) {
QRCode.value = res.data
ifShow.value = true
} else {
ifShow.value = false
}
uni.hideLoading();
})
}
</script>
<style scoped lang="scss">
@import '@/static/style/theme.scss';
.container {
height: 100vh;
background: linear-gradient(0.00deg, var(--left-linear) 0%,var(--right-linear) 94.004%);
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.box {
position: relative;
width: 680rpx;
border-image-width: 14rpx;
border-image-slice: 72;
border-image-repeat: repeat;
border-image-outset: 20rpx;
background: #fff;
border-radius: 26rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.top-box {
width: 710rpx;
background: linear-gradient(180.00deg, var(--left-linear), var(--right-linear) 100%);
border-radius: 26rpx;
margin-top: -2rpx;
display: flex;
align-items: center;
padding: 40rpx;
& > .number {
padding: 0 40rpx;
color: var(--text-color);
font-size: 64rpx;
font-weight: bold;
width: 50%;
line-height: 1;
word-break: break-all;
}
& > .detail {
color: var(--text-color);
display: flex;
flex-direction: column;
font-size: 24rpx;
font-weight: 500;
line-height: 1.5;
}
&::before {
content: "";
position: absolute;
top: -7px;
left: 4px;
width: 100%;
height: 14px;
background: radial-gradient(var(--right-linear) 0px, var(--right-linear) 5px, transparent 5px, transparent);
background-size: 14px 14px;
}
}
.paw {
width: 600rpx;
height: 600rpx;
background: #F2F2F2;
margin-top: 74rpx;
display: flex;
justify-content: center;
align-items: center;
}
.tip {
font-size: 27rpx;
font-weight: 500;
margin-top: 34rpx;
text-align: center;
color: var(--blackOrWhite);
letter-spacing: 1px;
}
.refresh {
width: 520rpx;
position: relative;
background: var(--btn-color1);
border-radius: 12rpx;
padding: 14rpx 0;
margin: 50rpx 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: var(--text-color);
::v-deep .uv-icon {
margin-right: 30rpx;
}
}
.container.golden-theme {
background: #FFFFFF;
::v-deep .back .left span,
::v-deep .back .left text {
color: var(--bg-color) !important;
}
.box {
background: var(--bg-color);
}
.top-box::before {
content: "";
position: absolute;
top: -7px;
left: 4px;
width: 100%;
height: 14px;
background: radial-gradient(#FFFFFF 0px, #FFFFFF 5px, transparent 5px, transparent);
background-size: 14px 14px;
z-index: 9;
}
}
</style>

1569
pages/orderdetail/index.vue Normal file

File diff suppressed because it is too large Load Diff

259
pages/orderdetail/lock.vue Normal file
View File

@ -0,0 +1,259 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="box">
<view class="top-box">
<view class="number">
{{ lockDetail.lockerName }}
</view>
<view class="detail">
{{ $t("detail.store") }} {{ lockDetail.siteName }} <br>
{{ $t("detail.unit") }} {{ lockDetail.unitTypeName }}<br>
<view v-if="lockDetail.orderStartStatus == 2 && lockDetail.refundLockerStatus === 0">{{ $t("detail.remain", { days: lockDetail.remainingDays }) }}</view>
<view style="color: red;" v-else>
{{ $t('common.status') }}
<template v-if="lockDetail.refundLockerStatus === 0">{{ $t("common.notStarted") }}</template>
<template v-else-if="lockDetail.refundLockerStatus === 1">{{ $t("unlock.cancelPending") }}</template>
<template v-else-if="lockDetail.refundLockerStatus === 2">{{ $t("unlock.outComplete") }}</template>
<template v-else-if="lockDetail.refundLockerStatus === 3">{{ $t("unlock.disapproval") }}</template>
</view>
</view>
</view>
<template v-if="!state.isTTlock || (state.tryPWDCount > tryPWDCountMAX)">
<view class="paw">
{{ lockPwd?.password }}{{ lockPwd?.password?'#':'' }}
</view>
<view class="tip">
{{ $t("door.pwd") }} <br>
{{ $t("door.valid") }}
</view>
</template>
<button class="refresh" @click="RemoteOpen" v-if="state.isTTlock">
{{ $t("door.Unlock") }}
</button>
<button class="refresh" @click="GetLockPwd" v-if="!state.isTTlock || (state.tryPWDCount > tryPWDCountMAX)">
<uv-icon name="reload" size="24" :color="themeInfo.iconColor"></uv-icon>
{{ $t("door.refreshPwd") }}
</button>
</view>
</view>
</template>
<script setup>
import {
reactive,
ref
} from 'vue';
import navBar from '@/components/navBar.vue'
import { onLoad } from '@dcloudio/uni-app'
import { useOrderApi } from '@/Apis/order.js'
import { useLockApi } from '@/Apis/lock.js'
//
import { useMainStore } from "@/store/index.js";
//
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { themeInfo } = useMainStore();
const getApi = useOrderApi()
const getLockApi = useLockApi()
const orderId = ref()
const lockMac = ref()
const lockDetail = ref([])
const lockPwd = ref([])
const tryPWDCountMAX = 3
const state = reactive({
isTTlock: false, // TT使,
tryPWDCount: 0, // ,
})
onLoad((params) => {
orderId.value = params.id
lockMac.value = params.lockmac
state.tryPWDCount = 0
GetOrderDetail()
})
//
function GetOrderDetail() {
uni.showLoading()
getApi.GetOrderById({ orderId: orderId.value }).then(res=>{
lockDetail.value = res.data
if([3, 7].includes(Number(lockDetail.value.lockTypeId))){
state.isTTlock = true
}else{
GetLockPwd()
}
})
uni.hideLoading()
}
//
function RemoteOpen() {
uni.showLoading({mask:true})
getLockApi.RemoteOpen({ lockMac: lockMac.value,input: '',siteId:lockDetail.value.siteId }).then(res => {
state.tryPWDCount++
if(res.data.isSuccess) {
uni.showToast({
title:t('door.UnlockSuccessful'),
duration:3000,
icon:'none'
})
uni.hideLoading()
} else {
uni.hideLoading()
uni.showToast({
title:res.data.message,
duration:3000,
icon:'none'
})
}
})
}
//
function GetLockPwd() {
uni.showLoading({mask:true})
getLockApi.GetDyncPwdByMac({ lockMac: lockMac.value,siteId:lockDetail.value.siteId }).then(res => {
state.tryPWDCount++
if(!res.data.password ) {
uni.hideLoading()
uni.showToast({
title:res.data.message,
duration:3000,
icon:'none'
})
} else {
uni.hideLoading()
lockPwd.value = res.data
}
})
}
</script>
<style scoped lang="scss">
@import '@/static/style/theme.scss';
.container {
height: 100vh;
background: linear-gradient(0.00deg, var(--left-linear) 0%, var(--right-linear) 94.004%);
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.box {
position: relative;
width: 680rpx;
border-image-width: 14rpx;
border-image-slice: 72;
border-image-repeat: repeat;
border-image-outset: 20rpx;
background: #fff;
border-radius: 26rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.top-box {
width: 710rpx;
background: linear-gradient(180.00deg, var(--left-linear), var(--right-linear) 100%);
border-radius: 26rpx;
margin-top: -2rpx;
display: flex;
align-items: center;
padding: 40rpx;
& > .number {
padding: 0 40rpx;
color: var(--text-color);
font-size: 64rpx;
font-weight: bold;
width: 50%;
line-height: 1;
word-break: break-all;
}
& > .detail {
flex: 1;
color: var(--text-color);
display: flex;
flex-direction: column;
font-size: 24rpx;
font-weight: 500;
line-height: 1.5;
}
&::before {
content: "";
position: absolute;
top: -7px;
left: 4px;
width: 100%;
height: 14px;
background: radial-gradient(var(--right-linear) 0px, var(--right-linear) 5px, transparent 5px, transparent);
background-size: 14px 14px;
}
}
.paw {
width: 556.16rpx;
height: 128rpx;
margin-top: 78rpx;
background: var(--active-color);
border-radius: 64rpx;
display: flex;
justify-content: center;
align-items: center;
letter-spacing: 8px;
font-size: 65rpx;
font-weight: 700;
color: var(--text-color);
}
.tip {
margin-top: 80rpx;
font-size: 27rpx;
font-weight: 500;
text-align: center;
color: var(--blackOrWhite);
letter-spacing: 1px;
}
.refresh {
width: 520rpx;
position: relative;
background: var(--btn-color1);
border-radius: 12rpx;
padding: 14rpx 0;
margin: 50rpx 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: var(--text-color);
::v-deep .uv-icon {
margin-right: 30rpx;
}
}
.container.golden-theme {
background: #FFFFFF;
::v-deep .back .left span,
::v-deep .back .left text {
color: var(--bg-color) !important;
}
.box {
background: var(--bg-color);
}
.top-box::before {
content: "";
position: absolute;
top: -7px;
left: 4px;
width: 100%;
height: 14px;
background: radial-gradient(#FFFFFF 0px, #FFFFFF 5px, transparent 5px, transparent);
background-size: 14px 14px;
z-index: 9;
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<view class="page">
<web-view
@load="loadDown"
@error="onError"
style="width: 100%; height: 94vh"
:fullscreen="false"
:src="webUrl">
</web-view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
let webUrl = ref('');
onMounted(() => {
let openId = uni.getStorageSync("openId");
webUrl.value = `https://www.elitesys.hk/EliteBotWeb/#/?wechatId=${openId}`;
uni.showLoading();
setTimeout(() => {
uni.hideLoading();
}, 1000);
});
const loadDown = () => {
uni.hideLoading();
};
const onError = () => {
uni.hideLoading();
};
</script>
<style lang="scss" scoped>
.page {
display: flex;
flex-direction: column;
height: 100vh;
background: #FFFFFF;
overflow: hidden;
}
</style>

123
pages/personal/index.vue Normal file
View File

@ -0,0 +1,123 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<view class="orderInfo sflist card">
<ul>
<li @click="goOrder">
<view class="left"><text class="font28 fontb">订单详情</text></view>
<view class="right font36 fontb textGary">
<uv-icon color="#000000" blod name="arrow-right" size="24rpx" />
</view>
</li>
<li @click="goVaild">
<view class="left"><text class="font28 fontb">信息验证</text></view>
<view class="right font36 fontb textGary">
<uv-icon color="#000000" blod name="arrow-right" size="24rpx" />
</view>
</li>
<!-- <li>
<view class="left"><text class="font28 fontb">发票申请</text></view>
<view class="right font36 fontb textGary">
<uv-icon color="#000000" blod name="arrow-right" size="32" />
</view>
</li> -->
<li @click="makePhoneCall">
<view class="left"><text class="font28 fontb">客服咨询</text></view>
<view class="right font36 fontb textGary">
<uv-icon color="#000000" blod name="arrow-right" size="24rpx" />
</view>
</li>
</ul>
</view>
<div class="footer">
<myCustomtTabBar direction="horizontal" :show-icon="true" :selected="2" @onTabItemTap="onTabItemTap">
</myCustomtTabBar>
</div>
</view>
</template>
<script setup>
import { ref } from 'vue';
import {
onTabItemTap,
makePhoneCall,
} from '/utils/common.js'
import { navigateTo } from '@/utils/navigateTo';
import myCustomtTabBar from '@/components/myCustomtTabBar.vue';
import myModal from "@/components/myModal.vue";
import { useLoginApi } from "@/Apis/login.js";
import { useOrderApi } from '@/Apis/order.js';
import { authInfoApi } from "@/Apis/validInfo.js";
import { shareParam } from "@/utils/common.js";
import { useI18n } from 'vue-i18n';
//
import { useMainStore } from "@/store/index.js"
import { onLoad,onShow,onShareAppMessage} from '@dcloudio/uni-app'
import coupon from '@/components/coupon.vue';
import inviteDetail from '@/components/inviteDetail.vue';
import MediatorinviteDetail from '@/components/MediatorinviteDetail.vue';
import { useRecommend } from "@/Apis/recommend.js";
import { baseImageUrl, projectInfo, isKingKong,envVersion } from '@/config';
const { themeInfo, storeState, getUserInfo,logOut } = useMainStore();
const getLoginApi = useLoginApi();
const getApi = useOrderApi();
const couponRef = ref();
const getRecommendApi = useRecommend();
const getAuthApi = authInfoApi();
const { t } = useI18n();
uni.hideTabBar();
const goVaild = () => {
navigateTo('/pagesb/validationInfo/index');
}
const goOrder = () => {
navigateTo('/pages/unlock/index');
}
</script>
<style lang="scss" scoped>
@import '@/static/style/theme.scss';
.container {
width: 100%;
min-height: 100vh;
padding-bottom: 400rpx;
display: flex;
flex-direction: column;
align-items: center;
.orderInfo {
padding: 0;
ul {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
padding: 28rpx;
justify-content: space-between;
border-bottom: 1px solid #a8aaac69;
&:active {
background-color: #f5f5f5;
}
&:last-child {
border: none;
}
.right {
display: flex;
text {
padding-right: 20px;
}
}
}
}
}
}
</style>

10
pages/phone/index.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<view class="container">
</view>
</template>
<script setup >
</script>
<style lang="scss" scoped>
</style>

305
pages/register/index.vue Normal file
View File

@ -0,0 +1,305 @@
<template>
<view class="register-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="container">
<view class="logoBox">
<image src="/static/logo.png" mode=""></image>
<text>EMPOWER YOUR SELF STORAGE</text>
</view>
<view class="formBox">
<view class="form">
<uv-form labelPosition="left" :model="state" labelWidth='160rpx' :rules="rules" ref="formRef">
<uv-form-item :label="$t('login.account')" prop="username" borderBottom>
<uv-input v-model="state.username" :placeholder="$t('login.account')" border="none">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.password')" prop="password" borderBottom >
<uv-input v-model="state.password" :placeholder="$t('login.password')" :type='state.passwordVisible?"text":"password"' border="none">
<template #suffix>
<uv-icon @click="state.passwordVisible = !state.passwordVisible" :name="state.passwordVisible?'eye-off-outline':'eye'" size="18" ></uv-icon>
</template>
</uv-input>
</uv-form-item>
<uv-form-item labelWidth='300rpx' :label="$t('login.confirm')" prop="passwordTow" borderBottom >
<uv-input v-model="state.passwordTow" :placeholder="$t('login.password')" :type='state.passwordVisible?"text":"password"' border="none">
<template #suffix>
<uv-icon @click="state.passwordVisible = !state.passwordVisible" :name="state.passwordVisible?'eye-off-outline':'eye'" size="18" ></uv-icon>
</template>
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('login.code')" prop="code" borderBottom >
<uv-input v-model="state.code" :placeholder="$t('login.code')" border="none">
</uv-input>
<template #right>
<uv-button size="small" type="primary" @click="send">{{ count? count+'S' : $t('login.send') }}</uv-button>
</template>
</uv-form-item>
<view>
<uv-button shape="circle" block type="primary" @click="onSubmit">{{ $t("login.registered") }}</uv-button>
<view class="goLogin" @click="goLogin">
{{ $t("login.toLogin") }}
</view>
</view>
</uv-form>
</view>
<!-- <van-form class="form" @submit="onSubmit" ref='fromRef'>
<van-cell-group inset>
<van-field v-model="state.username" name="account" label="account" placeholder="account"
:rules="[{ required: true, message: 'Please input' }]" />
<van-field v-model="state.password" type="password" name="password" label="password"
:right-icon="state.passwordVisible ? 'eye-o' : 'eye-off-o'" placeholder="password"
:rules="[{ required: true, message: 'Please input' }]" />
<van-field v-model="state.passwordTow" type="password" name="passwordTow" label="confirm password"
:right-icon="state.passwordVisible ? 'eye-o' : 'eye-off-o'" placeholder="password"
:rules="[{ required: true, message: 'Please input' },{ validator: passwordIsSame, message: 'The password is different.' }]" />
<van-field v-model="state.code" name="code" label="code" placeholder="code"
:rules="[{ required: true, message: 'Please input' }]">
<template #button>
<van-button size="small" type="primary"
@click="send">{{count? count+'S' : 'SEND'}}</van-button>
</template>
</van-field>
<div>
<van-button round block type="primary" native-type="submit">
registered
</van-button>
<view class="goLogin" @click="goLogin">
LOGIN AN ACCOUNT
</view>
</div>
</van-cell-group>
</van-form> -->
</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue';
import {
onTabItemTap
} from '/utils/common.js'
import {
useLoginApi
} from '@/Apis/login.js';
import {
navigateBack
} from '@/utils/common.js'
import {
useCountDown
} from "@/hooks/index";
import navBar from '@/components/navBar.vue'
//
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
//
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const state = ref({
codeImage: '',
username: '',
uuid: '',
code: '',
password: '',
passwordTow: '',
passwordVisible: false,
})
const passwordIsSame = () => state.value.password === state.value.passwordTow
const rules = {
username:[{ required: true, message: t("login.input"), trigger: ['blur', 'change'] }],
password:[{ required: true, message: t("login.input"), trigger: ['blur', 'change']}],
passwordTow:[{ required: true, message: t("login.input"), trigger: ['blur', 'change']}, { validator: passwordIsSame, message: t("login.different"), trigger: ['blur', 'change']}],
code:[{ required: true, message: t("login.input"), trigger: ['blur', 'change']}],
}
const getApi = useLoginApi()
const {
count,
countDown,
cancelCout
} = useCountDown();
const formRef = ref()
const togglePasswordVisibility = () => {
state.value.passwordVisible = !state.value.passwordVisible
};
const goLogin = () => {
navigateBack('/pages/login/index')
}
const ForgotPassword = () => {
getApi.ForgotPassword(`"${state.value.username}"`).then(res => {
console.log(res)
if (res.code === 200) {
}
})
}
const EmailVerify = () => {
uni.showLoading()
getApi.EmailVerify({
emailAddress: state.value.username,
verifyCode: state.value.code,
}).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title:res.msg,
icon:'none'
})
navigateBack('/pages/login/index')
}
})
}
const send = () => {
Register()
}
const Register = () => {
formRef.value.validateField(['username', 'password', 'passwordTow'],vaild => {
if (!vaild.length) {
uni.showLoading()
getApi.Register({
emailAddress: state.value.username,
password: state.value.password,
type: 0,
}).then(res => {
uni.hideLoading()
if (res.code === 200) {
uni.showToast({
title:res.msg,
icon:'none'
})
} else {
}
})
}
})
}
const onSubmit = (values) => {
formRef.value.validate().then(res=>{
EmailVerify()
})
};
</script>
<style scoped lang="scss">
@import '@/static/style/theme.scss';
.register-wrap {
background: linear-gradient(0deg, var(--left-linear) 0%, var(--right-linear) 94.004%);
height: 100vh;
}
.container {
padding: 20rpx;
font-size: 14px;
line-height: 24px;
min-width: 100%;
.logoBox {
margin-top: 6%;
padding: 40upx;
display: flex;
flex-direction: column;
color: #FFFFFF;
font-weight: 900;
font-size: 34rpx;
image {
margin-top: 20rpx;
width: 256rpx;
height: 63rpx;
}
}
.formBox {
position: fixed;
bottom: 0;
width: 100%;
left: 0;
padding: 0 20rpx;
.form {
background: #FFFFFF;
padding: 60rpx 20rpx;
border-radius: 24rpx 24rpx 0 0;
.goLogin {
text-align: center;
padding: 30rpx 0;
}
::v-deep .uv-form-item__body{
margin-bottom: 25rpx;
background-color: #F6F7F9;
overflow: visible;
padding: 10px 16px;
.uv-form-item__body__left{
width: auto;
min-width: 140rpx;
flex-wrap: nowrap;
color: #617986;
font-weight: 600;
white-space: nowrap;
display: flex;
align-items: center;
}
}
// ::v-deep .van-field {
// margin-bottom: 25rpx;
// background-color: #F6F7F9;
// overflow: visible;
// .van-field__label {
// width: auto;
// min-width: 120rpx;
// flex-wrap: nowrap;
// color: #617986;
// font-weight: 600;
// white-space: nowrap;
// display: flex;
// align-items: center;
// }
// .van-field__error-message {
// position: absolute;
// top: 40rpx;
// }
// .van-field__value {
// position: relative;
// }
// .van-field__control {
// color: #121819;
// font-weight: 600;
// }
// .van-field__button {
// .van-button {
// height: 50rpx;
// }
// }
// }
}
::v-deep .uv-button {
border-color: var(--btn-color5);
background-color: var(--btn-color5);
}
}
}
.code {
width: 140rpx;
height: 56rpx;
}
</style>

1154
pages/renewOrder/index.vue Normal file

File diff suppressed because it is too large Load Diff

1770
pages/setOrder/index.vue Normal file

File diff suppressed because it is too large Load Diff

350
pages/site/index.vue Normal file
View File

@ -0,0 +1,350 @@
<template>
<view class="container">
<view class="selectSite card" @click="openCityPicker">
<view class="left">
<image src="/static/home/address.svg" mode="widthFix" class="icon"></image>
<text class="address">{{state.city}}{{state.district?'·'+state.district:''}}</text>
</view>
<view class="right">
<text class="more">更多分店</text>
<uv-icon name="arrow-right" bold size="24rpx" fontb />
</view>
</view>
<view class="content">
<view class="siteList card" v-for="(item,index) in siteData.list" :key="item.id" @click="toHome(item)">
<view class="box">
<view class="left">
<view class="name font36 fontb">
<view class="tuijian">推荐</view> <text>{{ item.name }}</text> <view v-if="item.distance&& index === 0" class="zuijin">距离最近</view>
</view>
<view class="tagsList">
<view class="tag">随存随取</view>
<!-- <view class="tag">随存随取</view> -->
</view>
</view>
<view class="distance" @click.stop="handleNavigate(item)">
<view class="icon">
<image src="/static/site/map.svg"></image>
</view>
<view class="m" v-if="item.distance">
{{ item.distance }} KM
</view>
</view>
</view>
<view class="box2">
<view class="address fontb font28 textGary">{{ item.city }}{{ item.district }}{{ item.address }}</view>
<view class="time adress fontb font28 textGary">
营业: 00:00-24:00
</view>
<view class="phone adress fontb font28 textGary">
<text @click.stop="makePhoneCall">电话: 400-818-1813</text>
</view>
</view>
</view>
</view>
<uv-picker ref="cityPicker" confirmColor="#FB322E" :columns="[state.cityData]" @confirm="cityConfirm"></uv-picker>
<view class="footer">
<view @click="toHome">
返回主页
</view>
<view @click="openCityPicker">
选择城市
</view>
</view>
</view>
</template>
<script setup>
import { ref,reactive } from "vue";
import { onLoad, onShow, onShareAppMessage } from "@dcloudio/uni-app";
import { getDistance,mergeFiveGoatStores,makePhoneCall} from "@/utils/common.js";
import { useSiteApi } from "@/Apis/site.js";
import { useLoginApi } from "@/Apis/login.js";
import { ClientSite } from "@/Apis/book.js";
//
import { useMainStore } from "@/store/index.js";
//
import { useI18n } from "vue-i18n";
import { useLocation } from "@/hooks/useLocation";
const { t } = useI18n();
// const getLockApi = useLockApi();
const { locationState, getLocation } = useLocation();
const { themeInfo, getUserInfo, storeState } = useMainStore();
const getApi = ClientSite();
const getLoginApi = useLoginApi();
const cityPicker = ref(null);
const state = ref({
city: "全部",
district: "",
cityData: [],
areaData: [],
firstLoad: false,
});
let siteData = reactive({
list: [],
isLoading: false,
});
// 线
const handleNavigate = (item) => {
SFUIP.openLocation({
latitude: Number(item.latitude),
longitude: Number(item.longitude),
name: item.name,
address: item.address,
});
};
const toHome = (item) => {
uni.$emit('homeSiteId', { id: item.id }); //
uni.switchTab({
url: `/pages/index/index`,
});
}
const cityConfirm = (e) => {
console.log(e, "confirm");
state.value.city = e.value[0];
getSiteDetail();
}
const openCityPicker = () => {
cityPicker.value.open();
}
const closeCityPicker = () => {
cityPicker.value.close();
}
onLoad((params) => {
state.firstLoad = true;
getCityData();
});
const getCityData = () => {
getApi.GetCityAll().then(res => {
if(res.code === 200) {
state.value.cityData = res.data;
state.value.cityData.unshift("全部");
//
state.value.city = state.value.cityData[0];
}
})
}
// noLocation
const filterSiteData = (noLocation) => {
state.firstLoad = false;
if (!siteData.list.length) return;
let city = siteData.list[0]['city'];
// appid
if(noLocation){
city = siteData.list.find(item => item.city.indexOf("广州") !== -1)?.city;
}
siteData.list = siteData.list.filter((item) => item.city.indexOf(city) !== -1);
state.value.city = city;
getApi.GetDistrictByCity(city).then(res => {
if (res.code === 200) {
state.value.areaData = res.data;
state.value.areaData.unshift("全部");
// state.value.district = state.value.areaData[0];
}
});
}
//
const getSiteDetail = () => {
uni.showLoading();
let city = state.value.city === "全部" ? "" : state.value.city;
let district = state.value.district === "全部" ? "" : state.value.district;
siteData.list = []
getApi.getSiteDetailsAll({
city,
district,
}).then(res => {
if (res.code === 200) {
siteData.list = mergeFiveGoatStores(res.data);
if (locationState?.latitude && locationState?.longitude) {
const { latitude, longitude } = locationState;
siteData.list.forEach(item => {
const { distance, number } = getDistance(latitude, longitude, item.latitude, item.longitude);
item.distance = distance;
item.distanceNumber = number;
});
siteData.list.sort((a, b) => a.distanceNumber - b.distanceNumber);
if (state.firstLoad) filterSiteData();
}else {
//
if(AppId === 'wxb20921dfdd0b94f4' || AppId === 'wx3c4ab696101d77d1') {
if (state.firstLoad) filterSiteData(true);
}
}
}
siteData.isLoading = false;
}).catch(err => {
siteData.isLoading = false;
}).finally(() => {
uni.hideLoading();
})
;
}
onShow(() => {
uni.showLoading({
mask: true,
})
getLocation().finally(() => {
uni.hideLoading();
getSiteDetail();
});
});
</script>
<style scoped lang="scss">
@import "@/static/style/theme.scss";
.container {
height: 100vh;
padding-bottom: 100rpx;
position: relative;
}
.selectSite {
height: 75rpx; //
display: flex;
justify-content: space-between; //
align-items: center; //
background: linear-gradient(90deg, #FFE8C4 0%, #FFFFFF 100%);
width: 100%;
font-size: 30rpx;
}
.selectSite .left {
display: flex;
align-items: center; //
}
.selectSite .left .icon {
width: 30rpx; //
height: 30rpx;
margin-right: 20rpx; //
}
.selectSite .left .address {
font-size: 28rpx;
word-wrap: break-word;
word-break: break-all;
flex: 1;
}
.selectSite .right {
display: flex;
align-items: center; //
}
.selectSite .right .more {
font-size: 28rpx;
color: #666;
margin-right: 8rpx; // text icon
}
.siteList{
display: flex;
flex-direction: column;
.box2{
display: flex;
flex-direction: column;
.address{
margin-bottom: 10rpx;
word-wrap: break-word;
word-break: break-all;
}
.time{
margin-bottom: 10rpx;
}
}
.box{
display: flex;
width: 100%;
margin-bottom: 20rpx;
.left{
flex: 1;
align-items: center;
.name{
display: flex;
align-items: center;
line-height: 1;
:deep(span){
vertical-align: text-top;
}
.tuijian{
color: #FFFFFF;
padding: 8rpx 20rpx;
border-radius: 8rpx;
background-color:#334E80;
font-size: 24rpx;
margin-right: 20rpx;
}
.zuijin{
color: #000000;
padding: 10rpx;
border-radius: 8rpx;
margin-left: 10rpx;
font-size: 16rpx;
background-color: #F2F3F8;
}
}
.tagsList{
display: flex;
flex-wrap: wrap;
margin-top: 10rpx;
.tag{
color: #D2021B;
background-color: #FBE8EB;
padding: 8rpx 12rpx;
border-radius: 8rpx;
font-size: 26rpx;
margin-right: 10rpx;
margin-top: 10rpx;
}
}
}
.distance{
display: flex;
flex-direction: column;
font-size: 20rpx;
.icon{
text-align: center;
}
image{
width: 30rpx;
height: 30rpx;
padding: 6px;
border-radius: 999rpx;
background-color: #F2F3F8;
}
}
}
}
.footer{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100rpx;
display: flex;
justify-content: space-around;
align-items: center;
background-color: #fff;
view{
width: 50%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
color: #000;
font-size: 30rpx;
font-weight: bold;
}
}
</style>

1472
pages/unlock/index.vue Normal file

File diff suppressed because it is too large Load Diff

42
pages/webview/web.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<view class="page-container">
<!-- <navBar class="navBar"></navBar> -->
<view class="webview-container">
<web-view :src="url"></web-view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app'
// import navBar from '@/components/navBar.vue';
const url = ref('');
onLoad((options) => {
if (options.url) {
url.value = decodeURIComponent(options.url);
}
});
</script>
<style>
.page-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.navBar {
z-index: 10; /* 确保 navBar 在 WebView 之上 */
}
.webview-container {
height: 200px;
z-index: 1;
}
uni-web-view.uni-webview--fullscreen{
pointer-events: none;
}
</style>

363
pagesb/AControl/index.vue Normal file
View File

@ -0,0 +1,363 @@
<template>
<view
class="container"
:class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]"
>
<navBar />
<view class="content">
<view class="info">
<view class="i-header">
<view class="tabbox">
<view
class="li"
>
{{ $t("common.facialData") }}
</view>
</view>
</view>
<view class="infobox">
<view class="personal">
<view class="select">
<view class="label"> * 上传{{ state.appText }} Upload Photo </view>
<view class="inputBox">
<view class="ImgUpload">
<view style="position: relative;">
<!-- <my-upload v-model="state.formData.idFile1" :show-edit="true">
</my-upload> -->
<view style="display: flex;justify-content: center;width: 350rpx;align-items: center;">
<uv-icon v-if="!state.formData.facePhoto" name="camera" color="#ceced0" size="160" @click="handelCorpImage"></uv-icon>
<image v-else :src="state.formData.facePhoto" mode="" @click="handelCorpImage"></image>
</view>
</view>
</view>
</view>
</view>
<view class="btn">
<button class="next-btn" @click="toggleEdit()">
{{ $t("common.auth") }}
</button>
</view>
</view>
</view>
</view>
</view>
<qf-image-cropper class="qf-image-cropper " :choosable='false' v-show="state.showCrop" ref="qfImageCropperRef" :width="300" :height="400" :checkRange="true" :showAngle="true" @crop="handleCrop"></qf-image-cropper>
<!-- 是否验证成功 -->
<myModal v-model="state.showAuthModal" @confirm="confirm" content="授权成功" :cancelShow="false"></myModal>
</view>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import QfImageCropper from '@/pagesb/components/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.vue';
import { onLoad } from "@dcloudio/uni-app";
import { baseImageUrl,RElEASE_DATE} from "@/config/index.js";
// import { navigateBack } from '@/utils/common.js';
import navBar from "@/components/navBar.vue";
// import myUpload from "@/components/myUpload.vue";
//
import { useMainStore } from "@/store/index.js";
import myModal from "@/components/myModal.vue";
const { themeInfo, storeState } = useMainStore();
import { useI18n } from 'vue-i18n';
import { useOrderApi } from "@/Apis/order.js";
const { t } = useI18n();
const app = getApp();
const pages = getCurrentPages()
const getApi = useOrderApi();
const qfImageCropperRef = ref()
const state = reactive({
showAuthModal: false,
showCrop:false,
contentTips:'', //
formData: {
orderId: '',
facePhoto:''
},
appText: "仓位货物"
});
const handelCorpImage = () => {
state.showCrop = true
qfImageCropperRef.value.resetData()
qfImageCropperRef.value.chooseImage({ sourceType: ['camera'],fail:()=>{
state.showCrop = false
}}
);
}
const handleCrop = (e) => {
state.showCrop = false
state.formData.facePhoto = e.tempFilePath
// uni.previewImage({
// urls: [e.tempFilePath],
// current: 0
// });
}
onLoad((event) => {
state.formData.orderId = event.id;
getInfo()
GetAppText()
});
const toggleEdit = () => {
verifyPerson()
}
const getInfo = () => {
uni.showLoading();
getApi.GetOrderAuthorizationFace({
orderId: state.formData.orderId,
}).then(res => {
uni.hideLoading();
if (res.code == 200) {
const today = new Date();
const target = new Date(2026, 0, 28); // 0
if((today > target)){
state.formData.facePhoto = baseImageUrl + res.data
};
}
});
}
const GetAppText = ()=>{
getApi.GetAppText({key:'APPTEXT'}).then(res=>{
if(res.code == 200){
if(state.formData.orderId){
state.appText = res.data
}
}else{
state.appText = "仓位货物"
}
})
}
const confirm = () => {
uni.reLaunch({
url: "/pages/unlock/index",
});
};
//
async function UploaderImage(url) {
let url1 = "";
try {
const res = await getApi.UploaderImage({ filePath: url });
// const jsonstr = JSON.parse(res);
url1 = res.data;
} catch (error) {
//
console.error("UploaderImage error:", error);
throw error; //
}
return url1;
}
//
const verifyPerson = async () => {
let facePhoto = state.formData.facePhoto
if(!facePhoto){
uni.showToast({
title: "请上传图片,再授权!",
icon: 'none'
})
return
}
if(state.formData.facePhoto.indexOf(baseImageUrl) ===-1){
try{
uni.showLoading();
facePhoto = await UploaderImage(state.formData.facePhoto)
facePhoto = baseImageUrl + facePhoto
}catch(e){
//TODO handle the exception
uni.showToast({
title: "上传失败,请稍后重新尝试",
icon: 'none'
})
uni.hideLoading()
return
}
}
uni.showLoading();
getApi.OrderAuthorization({
orderId: state.formData.orderId,
facePhoto: facePhoto.replace(baseImageUrl,""),
}).then(res => {
uni.hideLoading();
if (res.code == 200) {
state.showAuthModal = true;
}
});
}
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
.container {
margin: 0;
padding: 0 20upx;
width: 100%;
min-height: 100vh;
padding-bottom: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(
to bottom,
var(--right-linear),
var(--left-linear2)
);
background-attachment: fixed;
.qf-image-cropper {
z-index: 10;
}
.content {
width: 100%;
.infobox {
width: 100%;
padding: 20upx 30upx;
background-color: #ffffff;
position: relative;
&::before {
content: "";
z-index: 9;
position: absolute;
bottom: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(var(--left-linear2) 0px, var(--left-linear2) 5px, transparent 5px, transparent);
background-size: 14px 14px;
}
}
.btn {
margin-top: 20rpx;
margin-bottom: 20rpx;
padding:0 20rpx;
button {
font-size: 28rpx;
padding: 12rpx 0;
font-size: 30rpx;
font-weight: bold;
color: var(--text-color);
background: var(--active-color);
border-radius: 10rpx;
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.13);
}
}
.select {
border-bottom: 1px dashed #d8d8d857;
margin-bottom: 16rpx;
.label {
color: #bec2ce;
font-size: 26rpx;
}
.info {
color: #242e42;
font-size: 22rpx;
display: flex;
justify-content: space-between;
font-weight: bold;
padding: 0 18rpx;
opacity: 0.7;
&.Total {
margin-top: 10rpx;
color: #242e42;
opacity: 1;
}
}
.garyBox {
background-color: rgba(216, 216, 216, 0.3);
border-radius: 8rpx;
padding: 2rpx 10rpx;
font-size: 20rpx;
font-weight: bold;
color: rgb(36, 46, 66);
display: flex;
align-items: center;
justify-content: space-between;
margin: 10rpx 4rpx;
}
.inputBox {
display: flex;
justify-content: center;
align-items: center;
padding: 10rpx;
margin-left: 5px;
.ImgUpload {
display: flex;
margin: 10upx 0;
}
.value {
flex: 1;
display: flex;
flex-wrap: wrap;
color: #242e42;
font-size: 24rpx;
font-weight: bold;
}
.arrow {
width: auto;
font-size: 24rpx;
.codeBtn {
font-size: 24rpx;
background-color: #d1cbcb2d;
}
}
}
}
}
.i-header {
position: relative;
&::before {
content: "";
position: absolute;
top: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(var(--right-linear) 0px, var(--right-linear) 5px, transparent 5px, transparent);
background-size: 14px 14px;
z-index: 9;
}
padding: 30upx 0;
width: 100%;
background-color: #f7f7f7;
color: #242e42;
.tabbox {
position: relative;
padding: 20upx 10%;
display: flex;
justify-content: space-around;
.li {
width: 50%;
background-color: transparent;
font-size: 32upx;
outline: none;
text-align: center;
}
.li.active {
font-weight: bold;
}
.bottom-line {
position: absolute;
left: calc(50% - 220rpx);
bottom: 0;
width: 160rpx;
height: 2.5px;
border-radius: 20px;
background: var(--main-color);
transition: left 0.5s ease;
&.right {
left: calc(50% + 60rpx);
}
}
}
}
}
</style>

17
pagesb/Apis/flashSale.js Normal file
View File

@ -0,0 +1,17 @@
import request from "/utils/request.js";
export default {
GetFlashSaleDistrict:(data) => {
return request.request({
url: '/ClientSite/GetFlashSaleDistrict',
method: 'get',
data,
});
},
GetFlashSaleInfo:(data) => {
return request.request({
url: '/ClientSite/GetFlashSaleInfo',
method: 'get',
data,
});
},
}

View File

@ -0,0 +1,173 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<navBar class="navBar"></navBar>
<view class="wrapbox">
<view class="container-form">
<view class="cHeader">
<view class="left"> 人品大爆发 </view>
<view class="right"> 已邀请人数 {{state.count}}/50 </view>
</view>
<view class="content">
<text class="detail">
{{ `活动内容邀请50位小伙伴关注[${projectInfo.name}]公众号,即可获得租仓优惠券!` }}
</text>
<view class="info">
<text> 使用门店全部门店 </text>
<text @click="makePhoneCall()">
{{ `详情咨询:${projectInfo.phone}` }}
</text>
</view>
</view>
<view class="footer">
<view class="left">
<button open-type="share" class="btn">去邀请</button>
<button class="btn" @click="makePhoneCall()">兑换</button>
</view>
<view class="right" @click="state.inviteShow = true"> 查看邀请记录>> </view>
</view>
</view>
<view class="tips">
{{ `*活动解释权归${projectInfo.name}所有` }}
</view>
</view>
<!-- 邀请详情 -->
<inviteDetail v-model="state.inviteShow"></inviteDetail>
</view>
</template>
<script setup>
import navBar from "@/components/navBar.vue";
import { makePhoneCall } from "/utils/common.js";
import { ref } from "vue";
import { useRecommend } from "@/Apis/recommend.js";
import { onLoad, onShareAppMessage,onShow } from "@dcloudio/uni-app";
//
import { useMainStore } from "@/store/index.js";
import { baseImageUrl, projectInfo } from "@/config/index.js";
import inviteDetail from '@/components/inviteDetail.vue';
const { themeInfo } = useMainStore();
const getApi = useRecommend();
const state = ref({
inviteShow: false,
count: 0
});
onLoad((params) => {});
onShow(() => {
getApi.GetRecommendCount().then((res) => {
console.log(res);
if(res.code === 200){
state.count = res.data;
}
});
});
onShareAppMessage((res) => {
if (res.from === "button") {
let userInfo = uni.getStorageSync("userInfo");
//
return {
title: `您的好友(${
userInfo?.customerName || "微信好友"
})邀请您一起体验${projectInfo.name}改变储物方式!`,
path: "pages/index/index?Pre_ID=" + userInfo?.customerNo, // ,
imageUrl: baseImageUrl + "2c5f7950-e842-47e5-a135-86d04f22bab8.png",
};
}
return shareParam;
});
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
uni-page-body {
height: 100%;
background: linear-gradient(0deg, rgb(1, 169, 188), rgb(10, 132, 184));
overflow: auto;
}
.navBar {
position: relative;
}
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: start;
width: 100%;
overflow-x: hidden;
height: 100vh;
background: linear-gradient(0deg, var(--left-linear), var(--right-linear));
.wrapbox {
padding: 0 20rpx;
}
}
.container-form {
width: 100%;
box-sizing: border-box;
background: white;
border-radius: 18rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 20rpx;
font-size: 24rpx;
.cHeader {
display: flex;
justify-content: space-between;
border-bottom: 1rpx solid #e5e5e5;
padding-bottom: 20rpx;
width: 100%;
font-weight: bold;
}
.content {
padding: 0 10rpx;
padding-top: 20rpx;
.detail {
margin: 40rpx 0;
font-weight: bold;
}
.info {
display: flex;
flex-direction: column;
margin-top: 20rpx;
}
}
.footer {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
padding: 10rpx 0;
padding-top: 40rpx;
font-weight: bold;
.left {
display: flex;
justify-content: space-between;
font-size: 22rpx;
.btn {
width: 160rpx;
height: 44rpx;
line-height: 44rpx;
padding: 0;
font-size: 22rpx;
margin-right: 10rpx;
border-radius: 999px;
border: 1rpx solid transparent;
&:nth-child(2) {
border-color: #000000;
}
&:nth-child(1) {
background-color: var(--main-color);
}
}
}
.right {
font-size: 20rpx;
}
}
}
.tips {
margin-top: 30rpx;
font-size: 20rpx;
font-weight: bold;
}
</style>

339
pagesb/changeUser/index.vue Normal file
View File

@ -0,0 +1,339 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]">
<nav-bar></nav-bar>
<view class="content">
<view class="openId-box">
<view class="row">
<text class="label">openId</text>
<uni-easyinput v-model="openId" placeholder="请输入 openId" :inputBorder="true" />
</view>
<view class="btn-row">
<uv-button @click="saveOpenId">写入 openId</uv-button>
<uv-button type="warning" @click="clearOpenId">清空 openId</uv-button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import navBar from "@/components/navBar.vue";
import { useMainStore } from "@/store/index.js";
import { useI18n } from "vue-i18n";
import { useLoginApi } from "@/Apis/login.js";
import { useLoginApi as homeApi } from "/Apis/home.js";
import { ClientSite } from "@/Apis/book.js";
const { themeInfo } = useMainStore();
const { t } = useI18n();
const getLoginApi = useLoginApi();
const getHomeApi = homeApi();
const getSiteApi = ClientSite();
const regionPickerRef = ref();
const typeSelectRef = ref();
const openId = ref("");
const state = reactive({
contentTips: t("reserve.contentTips"), //
formData: {
tag: 2,
userName: "",
phone: "",
type: "",
region: "", //
},
showTypeSelectModal: false,
columns: [],
});
const localOpenId = uni.getStorageSync("openId");
openId.value = localOpenId || "";
const saveOpenId = () => {
uni.setStorageSync("openId", openId.value || "");
if (openId.value) {
uni.showLoading({ title: "登录中..." });
getLoginApi.AuthorizedLogin({
openId: openId.value,
})
.then((res) => {
uni.hideLoading();
if (res.code == 200) {
uni.setStorageSync("token", res.data.token);
uni.setStorageSync("openId", res.data.openId);
uni.showToast({ title: "登录成功", icon: "none" });
} else {
uni.showToast({ title: res.msg, icon: "none" });
}
}).catch((err) => {
})
} else {
uni.showToast({ title: "请输入 openId", icon: "none" });
}
};
const clearOpenId = () => {
openId.value = "";
uni.setStorageSync("openId", "");
uni.showToast({ title: "已清空 openId", icon: "none" });
};
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
.typeSelectBox {
button {
font-size: 32rpx;
padding: 12rpx 0;
font-size: 32rpx;
padding: 12rpx 0;
color: var(--text-color);
background: var(--active-color);
border-radius: 10rpx;
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.13);
}
}
.container {
margin: 0;
padding: 0 20upx;
width: 100%;
min-height: 100vh;
padding-bottom: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(to bottom,
var(--right-linear),
var(--left-linear2));
background-attachment: fixed;
.content {
width: 100%;
.infobox {
width: 100%;
padding: 10upx 6% 60upx;
background-color: #ffffff;
position: relative;
&::before {
content: "";
z-index: 9;
position: absolute;
bottom: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(var(--left-linear2) 0px,
var(--left-linear2) 5px,
transparent 5px,
transparent);
background-size: 14px 14px;
}
}
.btn {
margin-top: 60rpx;
margin-bottom: 20rpx;
button {
font-size: 28rpx;
padding: 12rpx 0;
color: var(--text-color);
background: var(--active-color);
border-radius: 10rpx;
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.13);
}
}
.select {
border-bottom: 1px solid #bec2ce;
margin: 30rpx 0;
.label {
color: #bec2ce;
font-size: 26rpx;
font-weight: bold;
}
.info {
color: #242e42;
font-size: 22rpx;
display: flex;
justify-content: space-between;
font-weight: bold;
padding: 0 18rpx;
opacity: 0.7;
&.Total {
margin-top: 10rpx;
color: #242e42;
opacity: 1;
}
}
.garyBox {
background-color: rgba(216, 216, 216, 0.3);
border-radius: 8rpx;
padding: 2rpx 10rpx;
font-size: 20rpx;
font-weight: bold;
color: rgb(36, 46, 66);
display: flex;
align-items: center;
justify-content: space-between;
margin: 10rpx 4rpx;
}
.inputBox {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx;
margin-left: 5px;
.ImgUpload {
display: flex;
margin: 10upx 0;
::v-deep .uv-upload {
margin-right: 40rpx;
border-radius: 18rpx;
border: 2px solid #9797974f;
.uv-upload__wrap__preview {
margin: 0;
}
}
.upLoadText {
font-size: 28upx;
text-align: center;
padding: 10upx 0;
background: var(--active-color);
border-radius: 0 0 18upx 18upx;
margin: -1px;
}
}
.value {
flex: 1;
display: flex;
flex-wrap: wrap;
color: #242e42;
font-size: 30rpx;
font-weight: bold;
}
.arrow {
width: auto;
font-size: 24rpx;
.codeBtn {
font-size: 24rpx;
background-color: #d1cbcb2d;
}
}
image {
width: 20rpx;
height: 12rpx;
}
}
}
}
.openId-box {
width: 100%;
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
margin-top: 20rpx;
.row {
display: grid;
grid-template-columns: 140rpx 1fr;
align-items: center;
gap: 16rpx;
.label {
font-size: 28rpx;
color: #666;
font-weight: 700;
}
}
.btn-row {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
::v-deep .uv-button {
flex: 1;
}
}
}
.i-header {
position: relative;
&::before {
content: "";
position: absolute;
top: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(var(--right-linear) 0px,
var(--right-linear) 5px,
transparent 5px,
transparent);
background-size: 14px 14px;
z-index: 9;
}
padding: 40upx 0;
width: 100%;
background-color: #f7f7f7;
color: #242e42;
.tabbox {
position: relative;
padding-top: 10upx;
display: flex;
justify-content: space-around;
.li {
width: 50%;
background-color: transparent;
font-size: 34upx;
outline: none;
text-align: center;
}
.li.active {
font-weight: bold;
}
.bottom-line {
position: absolute;
left: calc(50% - 220rpx);
bottom: 0;
width: 160rpx;
height: 2.5px;
border-radius: 20px;
background: var(--main-color);
transition: left 0.5s ease;
&.right {
left: calc(50% + 60rpx);
}
}
}
}
}
</style>

View File

@ -0,0 +1,5 @@
## 1.0.12023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.02023-05-10
uv-collapse 折叠面板

View File

@ -0,0 +1,60 @@
export default {
props: {
// 标题
title: {
type: String,
default: ''
},
// 标题右侧内容
value: {
type: String,
default: ''
},
// 标题下方的描述信息
label: {
type: String,
default: ''
},
// 是否禁用折叠面板
disabled: {
type: Boolean,
default: false
},
// 是否展示右侧箭头并开启点击反馈
isLink: {
type: Boolean,
default: true
},
// 是否开启点击反馈
clickable: {
type: Boolean,
default: true
},
// 是否显示内边框
border: {
type: Boolean,
default: true
},
// 标题的对齐方式
align: {
type: String,
default: 'left'
},
// 唯一标识符
name: {
type: [String, Number],
default: ''
},
// 标题左侧图片,可为绝对路径的图片或内置图标
icon: {
type: String,
default: ''
},
// 面板展开收起的过渡时间单位ms
duration: {
type: Number,
default: 300
},
...uni.$uv?.props?.collapseItem
}
}

View File

@ -0,0 +1,232 @@
<template>
<view class="uv-collapse-item">
<view
class="uv-collapse-item__content"
:animation="animationData"
ref="animation"
>
<view
class="uv-collapse-item__content__text content-class"
:id="elId"
:ref="elId"
><slot /></view>
</view>
<uv-cell
:title="title"
:value="value"
:label="label"
:icon="icon"
:isLink="isLink"
:clickable="clickable"
:border="parentData.border && showBorder"
@click="clickHandler"
cellStyle="background-color: var(--right-linear);padding:10px 20px;"
:arrowDirection="expanded ? 'up' : 'down'"
:disabled="disabled"
rightIconStyle="color: #000;font-size:20rpx;"
>
<!-- #ifndef MP-WEIXIN -->
<!-- 微信小程序不支持因为微信中不支持 <slot name="title" slot="title" />的写法 -->
<template slot="title">
<slot name="title"></slot>
</template>
<template slot="icon">
<slot name="icon"></slot>
</template>
<template slot="value">
<slot name="value"></slot>
</template>
<template slot="right-icon">
<slot name="right-icon"></slot>
</template>
<!-- #endif -->
</uv-cell>
<uv-line v-if="parentData.border"></uv-line>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
// #ifdef APP-NVUE
const animation = uni.requireNativePlugin('animation')
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* collapseItem 折叠面板Item
* @description 通过折叠面板收纳内容区域搭配uv-collapse使用
* @tutorial https://www.uvui.cn/components/collapse.html
* @property {String} title 标题
* @property {String} value 标题右侧内容
* @property {String} label 标题下方的描述信息
* @property {Boolean} disbled 是否禁用折叠面板 ( 默认 false )
* @property {Boolean} isLink 是否展示右侧箭头并开启点击反馈 ( 默认 true )
* @property {Boolean} clickable 是否开启点击反馈 ( 默认 true )
* @property {Boolean} border 是否显示内边框 ( 默认 true )
* @property {String} align 标题的对齐方式 ( 默认 'left' )
* @property {String | Number} name 唯一标识符
* @property {String} icon 标题左侧图片可为绝对路径的图片或内置图标
* @event {Function} change 某个item被打开或者收起时触发
* @example <uv-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</uv-collapse-item>
*/
export default {
name: "uv-collapse-item",
mixins: [mpMixin, mixin, props],
data() {
return {
elId: '',
// uni.createAnimation
animationData: {},
//
expanded: false,
// expandedbordercell线
showBorder: false,
//
animating: false,
// uv-collapse
parentData: {
accordion: false,
border: false
}
};
},
watch: {
expanded(n) {
clearTimeout(this.timer)
this.timer = null
// expandedcell线
this.timer = setTimeout(() => {
this.showBorder = n
}, n ? 10 : 290)
}
},
created() {
this.elId = this.$uv.guid();
},
mounted() {
this.init()
},
methods: {
//
init() {
//
this.updateParentData()
if (!this.parent) {
return this.$uv.error('uv-collapse-item必须要搭配uv-collapse组件使用')
}
const {
value,
accordion,
children = []
} = this.parent
if (accordion) {
if (this.$uv.test.array(value)) {
return this.$uv.error('手风琴模式下uv-collapse组件的value参数不能为数组')
}
this.expanded = this.name == value
} else {
if (!this.$uv.test.array(value) && value !== null) {
return this.$uv.error('非手风琴模式下uv-collapse组件的value参数必须为数组')
}
this.expanded = (value || []).some(item => item == this.name)
}
//
this.$nextTick(function() {
this.setContentAnimate()
})
},
updateParentData() {
// mixin
this.getParentData('uv-collapse')
},
async setContentAnimate() {
//
//
const rect = await this.queryRect()
const height = this.expanded ? rect.height : 0
this.animating = true
// #ifdef APP-NVUE
const ref = this.$refs['animation'].ref
animation.transition(ref, {
styles: {
height: height + 'px'
},
duration: this.duration,
// true
needLayout: true,
timingFunction: 'ease-in-out',
}, () => {
this.animating = false
})
// #endif
// #ifndef APP-NVUE
const animation = uni.createAnimation({
timingFunction: 'ease-in-out',
});
animation
.height(height)
.step({
duration: this.duration,
})
.step()
// animationData
this.animationData = animation.export()
//
this.$uv.sleep(this.duration).then(() => {
this.animating = false
})
// #endif
},
// collapsehead
clickHandler() {
if (this.disabled && this.animating) return
//
this.parent && this.parent.onChange(this)
},
//
queryRect() {
// #ifndef APP-NVUE
// this.$uvGetRectgetRect
return new Promise(resolve => {
this.$uvGetRect(`#${this.elId}`).then(size => {
resolve(size)
})
})
// #endif
// #ifdef APP-NVUE
// nvue使dom
// promise使then
return new Promise(resolve => {
dom.getComponentRect(this.$refs[this.elId], res => {
resolve(res.size)
})
})
// #endif
}
},
};
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
.uv-collapse-item {
&__content {
overflow: hidden;
height: 0;
&__text {
padding: 12px 15px;
color: $uv-content-color;
font-size: 14px;
line-height: 18px;
}
}
}
</style>

View File

@ -0,0 +1,20 @@
export default {
props: {
// 当前展开面板的name非手风琴模式[<string | number>]手风琴模式string | number
value: {
type: [String, Number, Array, null],
default: null
},
// 是否手风琴模式
accordion: {
type: Boolean,
default: false
},
// 是否显示外边框
border: {
type: Boolean,
default: true
},
...uni.$uv?.props?.collapse
}
}

View File

@ -0,0 +1,86 @@
<template>
<view class="uv-collapse">
<uv-line v-if="border"></uv-line>
<slot />
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* collapse 折叠面板
* @description 通过折叠面板收纳内容区域
* @tutorial https://www.uvui.cn/components/collapse.html
* @property {String | Number | Array} value 当前展开面板的name非手风琴模式[<string | number>]手风琴模式string | number
* @property {Boolean} accordion 是否手风琴模式 默认 false
* @property {Boolean} border 是否显示外边框 ( 默认 true
* @event {Function} change 当前激活面板展开时触发(如果是手风琴模式参数activeNames类型为String否则为Array)
* @example <uv-collapse></uv-collapse>
*/
export default {
name: "uv-collapse",
mixins: [mpMixin, mixin, props],
watch: {
needInit() {
this.init()
},
//
parentData() {
if (this.children.length) {
this.children.map(child => {
// (uv-checkbox)updateParentData()
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
},
created() {
this.children = []
},
computed: {
needInit() {
// computedaccordionvalue
// watchinit()
return [this.accordion, this.value]
}
},
methods: {
//
init() {
this.children.map(child => {
child.init()
})
},
/**
* collapse-item被点击时触发由collapse统一处理各子组件的状态
* @param {Object} target 被操作的面板的实例
*/
onChange(target) {
let changeArr = []
this.children.map((child, index) => {
//
if (this.accordion) {
child.expanded = child === target ? !target.expanded : false
child.setContentAnimate()
} else {
if(child === target) {
child.expanded = !child.expanded
child.setContentAnimate()
}
}
// change
changeArr.push({
// nameindex
name: child.name || index,
status: child.expanded ? 'open' : 'close'
})
})
this.$emit('change', changeArr)
this.$emit(target.expanded ? 'open' : 'close', target.name)
}
}
}
</script>

View File

@ -0,0 +1,89 @@
{
"id": "uv-collapse",
"displayName": "uv-collapse 折叠面板 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.1",
"description": "折叠面板组件,通过折叠面板收纳内容区域,点击可展开收起,多功能参数可配置。",
"keywords": [
"uv-collapse",
"uvui",
"uv-ui",
"collapse",
"折叠面板"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uv-ui-tools",
"uv-line",
"uv-cell"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"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",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,67 @@
## 2.2.42024-06-21
* 新增 reverseRotatable 属性,是否支持逆向翻转
* 修复 `2.1.7` 版本导致旋转后图片没有自动适配裁剪框的问题
## 2.2.32024-06-21
* 新增 gpu 属性,是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题
* 修复 组件使用 `v-if` 并设置 `src` 属性时可能会出现图片渲染位置存在偏差的问题
## 2.2.22024-06-21
* 优化 组件实例 chooseImage 方法支持传参
* 修复 组件使用 `v-if` 时组件无非正常渲染的问题
## 2.2.12024-06-15
* 修复 H5平台不支持手势拖动图片的问题
## 2.2.02024-05-31
* 修复 APP平台 `vue2` 项目因 `2.1.9` 版本修复 `vue3` 项目bug而引发的问题
## 2.1.92024-05-29
* 修复 APP平台 `vue3` 项目因 uniapp `renderjs` 中未支持条件编译导致运行了H5平台代码报错的问题
## 2.1.82024-05-29
* 新增 zIndex 属性,调整组件层级
* 新增 组件内容插槽
* 优化 微信小程序平台动态修改元素style时的多余内容
## 2.1.72024-05-28
* 新增 checkRange 属性,当 checkRange=false 时允许图片位置超出裁剪边界
* 新增 minScale 属性,图片最小缩放倍数,当 minScale<0 时可使图片宽高不再受裁剪区域宽高限制
* 新增 backgroundColor 属性,生成图片背景色,如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块
* 优化 动态修改图片宽高但没有传入src时尺寸适应问题
* 修复 APP平台通过 `this.$ownerInstance` 获取组件实例时机过早,其值为 `undefined` 导致报错界面没有正常渲染的问题
## 2.1.62023-04-16
* 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
## 2.1.52023-04-15
* 新增 兼容APP平台
## 2.1.42023-03-13
* 新增 fileType 属性,用于指定生成文件的类型,只支持 'jpg' 或 'png',默认为 'png'
* 新增 delay 属性,微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
* 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制1365*1365则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
* 优化 旋转图标指示方向与实际旋转方向不符
## 2.1.32023-02-06
* 优化 vue3支持
## 2.1.22023-02-03
* 新增 navigation 属性H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差
* 修复 H5平台部分设备已知iPhone11以下机型拍照的图片缩放时会闪动的问题
## 2.1.12022-12-06
* 修复 横屏适配问题
## 2.1.02022-12-06
* 新增 兼容H5平台使用 renderjs 响应手势事件
## 2.0.02022-12-05
* 重构 插件,使用 WXS 响应手势事件
* 新增 图片翻转
* 新增 拉伸裁剪框放大图片
* 新增 监听PC鼠标滚轮触发缩放
* 新增 圆形、圆角矩形的图片裁剪
* 优化 图片缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
* 优化 裁剪框样式
* 优化 图片位置拖动 支持边界回弹效果(滑动时可滑出边界,释放时回弹到边界)
* 优化 生成图片使用新版 Canvas 2D 接口

View File

@ -0,0 +1,738 @@
/**
* 图片编辑器-手势监听
* 1. 支持编译到app-vueuni-app 2.5.5及以上版本H5上
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/** 元素ID */
var elIds = {
'imageStyles': 'crop-image',
'maskStylesList': 'crop-mask-block',
'borderStyles': 'crop-border',
'circleBoxStyles': 'crop-circle-box',
'circleStyles': 'crop-circle',
'gridStylesList': 'crop-grid',
'angleStylesList': 'crop-angle',
}
/** 记录上次初始化时间戳排除APP重复更新 */
var timestamp = 0;
/** vue3 renderjs 条件编译无效,以此方式区别 APP 和 H5 */
// #ifdef H5
var platform = 'H5';
// #endif
// #ifdef APP
var platform = 'APP';
// #endif
/**
* 样式对象转字符串
* @param {Object} style 样式对象
*/
function styleToString(style) {
if(typeof style === 'string') return style;
var str = '';
for (let k in style) {
str += k + ':' + style[k] + ';';
}
return str;
}
/**
*
* @param {Object} instance 页面实例对象
* @param {Object} key 要修改样式的key
* @param {Object|Array} style 样式
*/
function setStyle(instance, key, style) {
// console.log('setStyle', instance, key, JSON.stringify(style))
// #ifdef APP-PLUS
if(platform === 'APP') {
if(Object.prototype.toString.call(style) === '[object Array]') {
for (var i = 0, len = style.length; i < len; i++) {
var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
el && (el.style = styleToString(style[i]));
}
} else {
var el = window.document.getElementById(elIds[key]);
el && (el.style = styleToString(style));
}
}
// #endif
// #ifdef H5
if(platform === 'H5') instance[key] = style;
// #endif
}
/**
* 触发页面实例指定方法
* @param {Object} instance 页面实例对象
* @param {Object} name 方法名称
* @param {Object} obj 传递参数
*/
function callMethod(instance, name, obj) {
// #ifdef APP-PLUS
if(platform === 'APP') instance.callMethod(name, obj);
// #endif
// #ifdef H5
if(platform === 'H5') instance[name](obj);
// #endif
}
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c);
}
/**
* 检查边界限制 xy 拖动范围禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
var o = (img.height - img.width) / 2; // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, img.height < area.height),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, img.width < area.width)
}
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
}
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
// console.log('changeImageRect', e)
offset.x += e.x || 0;
offset.y += e.y || 0;
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// 因频繁修改 width/height 会造成大量的内存消耗改为scale
// e.instance.imageStyles = {
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
// };
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
// e.instance.imageStyles = {
// width: img.oldWidth + 'px',
// height: img.oldHeight + 'px',
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
// };
setStyle(e.instance, 'imageStyles', {
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px' + ') rotate(' + rotate +'deg) scale(' + scale + ')'
});
callMethod(e.instance, 'dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// console.log('changeAreaRect', e)
// 变更蒙版样式
setStyle(e.instance, 'maskStylesList', [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
]);
// 变更边框样式
if(area.showBorder) {
setStyle(e.instance, 'borderStyles', {
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
setStyle(e.instance, 'gridStylesList', [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
]);
}
// 变更四个伸缩角样式
if(area.showAngle) {
setStyle(e.instance, 'angleStylesList', [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
]);
}
// 变更圆角样式
if(area.radius > 0) {
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
setStyle(e.instance, 'circleBoxStyles', {
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
setStyle(e.instance, 'circleStyles', {
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
// console.log('scaleImage', e)
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = img.oldWidth * scale;
img.height = img.oldHeight * scale;
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = (e.x - offset.x) * (1 - scale / last);
e.y = (e.y - offset.y) * (1 - scale / last);
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=1=左上2=右上3=左下4=右下
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
var oy = sys.navigation ? 0 : sys.windowTop;
if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
function getTouchs(touches) {
var result = [];
var len = touches ? touches.length : 0
for (var i = 0; i < len; i++) {
result[i] = {
pageX: touches[i].pageX,
// h5无标题栏时窗口顶部距离仍为标题栏高度且触摸点y轴坐标还是有标题栏的值即减去标题栏高度的值
pageY: touches[i].pageY + sys.windowTop
};
}
return result;
};
var mouseEvent = false;
export default {
data() {
return {
imageStyles: {},
maskStylesList: [{}, {}, {}, {}],
borderStyles: {},
gridStylesList: [{}, {}, {}, {}],
angleStylesList: [{}, {}, {}, {}],
circleBoxStyles: {},
circleStyles: {}
}
},
created() {
// 监听 PC 端鼠标滚轮
// #ifdef H5
platform === 'H5' && window.addEventListener('mousewheel', async (e) => {
var touchs = getTouchs([e])
img.src && scaleImage({
instance: await this.getInstance(),
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.deltaY > 0 ? -0.05 : 0.05,
x: touchs[0].pageX,
y: touchs[0].pageY
});
});
// #endif
},
// #ifdef H5
mounted() {
platform === 'H5' && this.initH5Events();
},
// #endif
setPlatform(p) {
platform = p;
},
methods: {
// #ifdef H5
getTouchEvent(e) {
e.touches = [
{ pageX: e.pageX, pageY: e.pageY }
];
return e;
},
initH5Events() {
const preview = document.getElementById('pic-preview');
preview?.addEventListener('mousedown', (e, ev) => {
mouseEvent = true;
this.touchstart(this.getTouchEvent(e));
});
preview?.addEventListener('mousemove', (e) => {
if (!mouseEvent) return;
this.touchmove(this.getTouchEvent(e));
});
preview?.addEventListener('mouseup', (e) => {
mouseEvent = false;
this.touchend(this.getTouchEvent(e))
});
preview?.addEventListener('mouseleave', (e) => {
mouseEvent = false;
this.touchend(this.getTouchEvent(e))
});
},
// #endif
async getInstance() {
// #ifdef APP-PLUS
if(platform === 'APP')
return this.$ownerInstance
? Promise.resolve(this.$ownerInstance)
: new Promise((resolve) => {
setTimeout(async () => {
resolve(await this.getInstance());
});
});
// #endif
// #ifdef H5
if(platform === 'H5')
return Promise.resolve(this);
// #endif
},
/**
* 初始化观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: async function(newVal, oldVal, o, i) {
// console.log('initObserver', newVal, oldVal, o, i)
if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
timestamp = newVal.timestamp;
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
const instance = await this.getInstance()
img.src && changeImageRect({
instance,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance
});
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
// h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = getTouchs(e.touches);
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', e, activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: async function(e, o) {
if(!img.src) return;
// console.log('touchmove', e, o)
e.touches = getTouchs(e.touches);
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = area.width * (1 - area.minScale);
var maxY = area.height * (1 - area.minScale);
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
switch(activeAngle) {
case 1: // 左上角
x += areaOffset.left;
y += areaOffset.top;
// console.log(x, y, offset.left > area.left)
// console.log(maxX, maxY)
if(x >= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, offset.x - area.left)
: false;
if(x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x += areaOffset.right;
y += areaOffset.top;
if(x <= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, area.right - offset.x - img.width)
: false;
if(-x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = -y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += areaOffset.left;
y += areaOffset.bottom;
if(x >= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, offset.x - area.left)
: false;
if(x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = -y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x += areaOffset.right;
y += areaOffset.bottom;
if(x <= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, area.right - offset.x - img.width)
: false;
if(-x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: await this.getInstance(),
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: await this.getInstance(),
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: await this.getInstance(),
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: async function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: await this.getInstance(),
});
scaleImage({
instance: await this.getInstance(),
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: await this.getInstance(),
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: async function(r) {
rotate = (rotate + (r || 90)) % 360;
if(img.minScale >= 1) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = (area.width / img.oldHeight)
}
if(minScale !== 1) {
scaleImage({
instance: await this.getInstance(),
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: await this.getInstance(),
check: true,
x: -ox - oy,
y: -oy + ox
});
},
rotateImage90: function() {
this.rotateImage(90)
},
rotateImage270: function() {
this.rotateImage(270)
},
}
}

View File

@ -0,0 +1,790 @@
<template>
<view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
width: `${canvansWidth}px`,
height: `${canvansHeight}px`,
}"></canvas>
<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
width: `${canvansWidth}px`,
height: `${canvansHeight}px`,
}"></canvas>
<view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData"
@touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp>
</image>
<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block"
:style="cropper.maskStylesList[index]"></view>
<view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
</view>
<block v-if="showGrid">
<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid"
:style="cropper.gridStylesList[index]"></view>
</block>
<block v-if="showAngle">
<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle"
:style="cropper.angleStylesList[index]">
<view :style="[
{
width: `${angleSize}px`,
height: `${angleSize}px`,
},
]"></view>
</view>
</block>
</view>
<slot />
<view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
<view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
<view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
</view>
<view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
<block v-else-if="!!imgSrc">
<view class="rechoose" @click="chooseImage">重选</view>
<button class="button" size="mini" @click="cropClick">确定</button>
</block>
<view v-else class="choose-btn" @click="chooseImage">选择图片</view>
</view>
</view>
</template>
<!-- #ifdef APP-VUE -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
// vue3 app renderjs
cropper.setPlatform('APP');
export default {
mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef H5 -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
export default {
mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-QQ -->
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
<!-- #endif -->
<script>
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
const AREA_SIZE = 75;
/** 图片默认宽高 */
const IMG_SIZE = 300;
export default {
name: "qf-image-cropper",
// #ifdef MP-WEIXIN
options: {
// 使 class
styleIsolation: "isolated",
},
// #endif
props: {
/** 图片资源地址 */
src: {
type: String,
default: "",
},
/** 裁剪宽度有些平台或设备对于canvas的尺寸有限制过大可能会导致无法正常绘制 */
width: {
type: Number,
default: IMG_SIZE,
},
/** 裁剪高度有些平台或设备对于canvas的尺寸有限制过大可能会导致无法正常绘制 */
height: {
type: Number,
default: IMG_SIZE,
},
/** 是否绘制裁剪区域边框 */
showBorder: {
type: Boolean,
default: true,
},
/** 是否绘制裁剪区域网格参考线 */
showGrid: {
type: Boolean,
default: true,
},
/** 是否展示四个支持伸缩的角 */
showAngle: {
type: Boolean,
default: true,
},
/** 裁剪区域最小缩放倍数 */
areaScale: {
type: Number,
default: 0.3,
},
/** 图片最小缩放倍数 */
minScale: {
type: Number,
default: 1,
},
/** 图片最大缩放倍数 */
maxScale: {
type: Number,
default: 5,
},
/** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
checkRange: {
type: Boolean,
default: true,
},
/** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
backgroundColor: {
type: String,
},
/** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
bounce: {
type: Boolean,
default: true,
},
/** 是否支持翻转 */
rotatable: {
type: Boolean,
default: true,
},
/** 是否支持逆向翻转 */
reverseRotatable: {
type: Boolean,
default: false,
},
/** 是否支持从本地选择素材 */
choosable: {
type: Boolean,
default: true,
},
/** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
gpu: {
type: Boolean,
default: false,
},
/** 四个角尺寸单位px */
angleSize: {
type: Number,
default: 20,
},
/** 四个角边框宽度单位px */
angleBorderWidth: {
type: Number,
default: 2,
},
zIndex: {
type: [Number, String],
},
/** 裁剪图片圆角半径单位px */
radius: {
type: Number,
default: 0,
},
/** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
fileType: {
type: String,
default: "png",
},
/**
* 图片从绘制到生成所需时间单位ms
* 微信小程序平台使用 `Canvas 2D` 绘制时有效
* 如绘制大图或出现裁剪图片空白等情况应适当调大该值 `Canvas 2d` 采用同步绘制需自己把控绘制完成时间
*/
delay: {
type: Number,
default: 1000,
},
// #ifdef H5
/**
* 页面是否是原生标题栏
* H5平台当 showAngle true 使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 必须将此值设为 false 否则四个可拉伸角的触发位置会有偏差
* 因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的
*/
navigation: {
type: Boolean,
default: true,
},
// #endif
},
emits: ["crop"],
data() {
return {
// id 使 v-for key
maskList: [
{ id: "crop-mask-block-1" },
{ id: "crop-mask-block-2" },
{ id: "crop-mask-block-3" },
{ id: "crop-mask-block-4" },
],
gridList: [
{ id: "crop-grid-1" },
{ id: "crop-grid-2" },
{ id: "crop-grid-3" },
{ id: "crop-grid-4" },
],
angleList: [
{ id: "crop-angle-1" },
{ id: "crop-angle-2" },
{ id: "crop-angle-3" },
{ id: "crop-angle-4" },
],
/** 本地缓存的图片路径 */
imgSrc: "",
/** 图片的裁剪宽度 */
imgWidth: IMG_SIZE,
/** 图片的裁剪高度 */
imgHeight: IMG_SIZE,
/** 裁剪区域最大宽度所占屏幕宽度百分比 */
widthPercent: AREA_SIZE,
/** 裁剪区域最大高度所占屏幕宽度百分比 */
heightPercent: AREA_SIZE,
/** 裁剪区域布局信息 */
area: {},
/** 未被缩放过的图片宽 */
oldWidth: 0,
/** 未被缩放过的图片高 */
oldHeight: 0,
/** 系统信息 */
sys: uni.getSystemInfoSync(),
scaleWidth: 0,
scaleHeight: 0,
rotate: 0,
offsetX: 0,
offsetY: 0,
use2d: false,
canvansWidth: 0,
canvansHeight: 0,
// imageStyles: {},
// maskStylesList: [{}, {}, {}, {}],
// borderStyles: {},
// gridStylesList: [{}, {}, {}, {}],
// angleStylesList: [{}, {}, {}, {}],
// circleBoxStyles: {},
// circleStyles: {},
};
},
computed: {
initData() {
// console.log('initData')
return {
timestamp: new Date().getTime(),
area: {
...this.area,
bounce: this.bounce,
showBorder: this.showBorder,
showGrid: this.showGrid,
showAngle: this.showAngle,
angleSize: this.angleSize,
angleBorderWidth: this.angleBorderWidth,
minScale: this.areaScale,
widthPercent: this.widthPercent,
heightPercent: this.heightPercent,
radius: this.radius,
checkRange: this.checkRange,
zIndex: +this.zIndex || 0,
},
sys: this.sys,
img: {
minScale: this.minScale,
maxScale: this.maxScale,
src: this.imgSrc,
width: this.oldWidth,
height: this.oldHeight,
oldWidth: this.oldWidth,
oldHeight: this.oldHeight,
gpu: this.gpu,
},
};
},
imgProps() {
return {
width: this.width,
height: this.height,
src: this.src,
};
},
},
watch: {
imgProps: {
handler(val, oldVal) {
//
this.imgWidth = Number(val.width) || IMG_SIZE;
this.imgHeight = Number(val.height) || IMG_SIZE;
let use2d = true;
// #ifndef MP-WEIXIN
use2d = false;
// #endif
// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
// use2d = false;
// }
let canvansWidth = this.imgWidth;
let canvansHeight = this.imgHeight;
let size = Math.max(canvansWidth, canvansHeight);
let scalc = 1;
if (size > 1365) {
scalc = 1365 / size;
}
this.canvansWidth = canvansWidth * scalc;
this.canvansHeight = canvansHeight * scalc;
this.use2d = use2d;
this.initArea();
const src = val.src || this.imgSrc;
src && this.initImage(src, oldVal === undefined);
},
immediate: true,
},
},
methods: {
/** 提供给wxs调用用来接收图片变更数据 */
dataChange(e) {
// console.log('dataChange', e)
this.scaleWidth = e.width;
this.scaleHeight = e.height;
this.rotate = e.rotate;
this.offsetX = e.x;
this.offsetY = e.y;
},
/** 初始化裁剪区域布局信息 */
initArea() {
// = +
this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
// #ifndef H5
this.sys.windowTop = 0;
this.sys.navigation = true;
// #endif
// #ifdef H5
// h5
this.sys.windowTop = this.sys.windowTop || 44;
this.sys.navigation = this.navigation;
// #endif
let wp = this.widthPercent;
let hp = this.heightPercent;
if (this.imgWidth > this.imgHeight) {
hp = (hp * this.imgHeight) / this.imgWidth;
} else if (this.imgWidth < this.imgHeight) {
wp = (wp * this.imgWidth) / this.imgHeight;
}
const size =
this.sys.windowWidth > this.sys.windowHeight
? this.sys.windowHeight
: this.sys.windowWidth;
const width = (size * wp) / 100;
const height = (size * hp) / 100;
const left = (this.sys.windowWidth - width) / 2;
const right = left + width;
const top =
(this.sys.windowHeight +
this.sys.windowTop -
this.sys.offsetBottom -
height) /
2;
const bottom =
this.sys.windowHeight +
this.sys.windowTop -
this.sys.offsetBottom -
top;
this.area = { width, height, left, right, top, bottom };
this.scaleWidth = width;
this.scaleHeight = height;
},
/** 从本地选取图片 */
chooseImage(options) {
// #ifdef MP-WEIXIN || MP-JD
if (uni.chooseMedia) {
uni.chooseMedia({
...options,
count: 1,
mediaType: ["image"],
success: (res) => {
this.resetData();
this.initImage(res.tempFiles[0].tempFilePath);
},
});
return;
}
// #endif
uni.chooseImage({
...options,
count: 1,
success: (res) => {
this.resetData();
this.initImage(res.tempFiles[0].path);
},
});
},
/** 重置数据 */
resetData() {
this.imgSrc = "";
this.rotate = 0;
this.offsetX = 0;
this.offsetY = 0;
this.initArea();
},
/**
* 初始化图片信息
* @param {String} url 图片链接
*/
initImage(url, isFirst) {
uni.getImageInfo({
src: url,
success: async (res) => {
if (isFirst && this.src === url)
await new Promise((resolve) => setTimeout(resolve, 50));
this.imgSrc = res.path;
let scale = res.width / res.height;
let areaScale = this.area.width / this.area.height;
if (scale > 1) {
//
if (scale >= areaScale) {
//
this.scaleWidth =
(this.scaleHeight / res.height) *
this.scaleWidth *
(res.width / this.scaleWidth);
} else {
//
this.scaleHeight = (res.height * this.scaleWidth) / res.width;
}
} else {
//
if (scale <= areaScale) {
//
this.scaleHeight =
((this.scaleWidth / res.width) * this.scaleHeight) /
(this.scaleHeight / res.height);
} else {
//
this.scaleWidth = (res.width * this.scaleHeight) / res.height;
}
}
//
this.oldWidth = this.scaleWidth;
this.oldHeight = this.scaleHeight;
},
fail: (err) => {
console.error(err);
},
});
},
/**
* 剪切图片圆角
* @param {Object} ctx canvas 的绘图上下文对象
* @param {Number} radius 圆角半径
* @param {Number} scale 生成图片的实际尺寸与截取区域比
* @param {Function} drawImage 执行剪切时所调用的绘图方法入参为是否执行了剪切
*/
drawClipImage(ctx, radius, scale, drawImage) {
if (radius > 0) {
ctx.save();
ctx.beginPath();
const w = this.canvansWidth;
const h = this.canvansHeight;
if (w === h && radius >= w / 2) {
//
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
} else {
//
if (w !== h) {
//
radius = Math.min(w / 2, h / 2, radius);
// radius = Math.min(Math.max(w, h) / 2, radius);
}
ctx.moveTo(radius, 0);
ctx.arcTo(w, 0, w, h, radius);
ctx.arcTo(w, h, 0, h, radius);
ctx.arcTo(0, h, 0, 0, radius);
ctx.arcTo(0, 0, w, 0, radius);
ctx.closePath();
}
ctx.clip();
drawImage && drawImage(true);
ctx.restore();
} else {
drawImage && drawImage(false);
}
},
/**
* 旋转图片
* @param {Object} ctx canvas 的绘图上下文对象
* @param {Number} rotate 旋转角度
* @param {Number} scale 生成图片的实际尺寸与截取区域比
*/
drawRotateImage(ctx, rotate, scale) {
if (rotate !== 0) {
// 1.
const x = (this.scaleWidth * scale) / 2;
const y = (this.scaleHeight * scale) / 2;
ctx.translate(x, y);
// 2.
ctx.rotate((rotate * Math.PI) / 180);
// 3.
ctx.translate(-x, -y);
}
},
drawImage(ctx, image, callback) {
//
const scale = this.canvansWidth / this.area.width;
if (this.backgroundColor) {
if (ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
else ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
}
this.drawClipImage(ctx, this.radius, scale, () => {
this.drawRotateImage(ctx, this.rotate, scale);
const r = this.rotate / 90;
ctx.drawImage(
image,
[
this.offsetX - this.area.left,
this.offsetY - this.area.top,
-(this.offsetX - this.area.left),
-(this.offsetY - this.area.top),
][r] * scale,
[
this.offsetY - this.area.top,
-(this.offsetX - this.area.left),
-(this.offsetY - this.area.top),
this.offsetX - this.area.left,
][r] * scale,
this.scaleWidth * scale,
this.scaleHeight * scale
);
});
},
/**
* 绘图
* @param {Object} canvas
* @param {Object} ctx canvas 的绘图上下文对象
* @param {String} src 图片路径
* @param {Function} callback 开始绘制时回调
*/
draw2DImage(canvas, ctx, src, callback) {
// console.log('draw2DImage', canvas, ctx, src, callback)
if (canvas) {
const image = canvas.createImage();
image.onload = () => {
this.drawImage(ctx, image);
// ````
callback && setTimeout(callback, this.delay);
};
image.onerror = (err) => {
console.error(err);
uni.hideLoading();
};
image.src = src;
} else {
this.drawImage(ctx, src);
setTimeout(() => {
ctx.draw(false, callback);
}, 200);
}
},
/**
* 画布转图片到本地缓存
* @param {Object} canvas
* @param {String} canvasId
*/
canvasToTempFilePath(canvas, canvasId) {
// console.log('canvasToTempFilePath', canvas, canvasId)
uni.canvasToTempFilePath(
{
canvas,
canvasId,
x: 0,
y: 0,
width: this.canvansWidth,
height: this.canvansHeight,
destWidth: this.imgWidth, //
destHeight: this.imgHeight, //
fileType: this.fileType, // png
success: (res) => {
//
this.handleImage(res.tempFilePath);
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: "裁剪失败,生成图片异常!", icon: "none" });
},
},
this
);
},
/** 确认裁剪 */
cropClick() {
uni.showLoading({ title: "裁剪中...", mask: true });
if (!this.use2d) {
const ctx = uni.createCanvasContext("imgCanvas", this);
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
this.draw2DImage(null, ctx, this.imgSrc, () => {
this.canvasToTempFilePath(null, "imgCanvas");
});
return;
}
// #ifdef MP-WEIXIN
const query = uni.createSelectorQuery().in(this);
query
.select("#imgCanvas")
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const dpr = uni.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
this.draw2DImage(canvas, ctx, this.imgSrc, () => {
this.canvasToTempFilePath(canvas);
});
});
// #endif
},
handleImage(tempFilePath) {
// H5tempFilePath base64
// console.log(tempFilePath)
uni.hideLoading();
this.$emit("crop", { tempFilePath });
},
},
};
</script>
<style lang="scss" scoped>
.image-cropper {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: #000;
.img-canvas {
position: absolute !important;
transform: translateX(-100%);
}
.pic-preview {
width: 100%;
flex: 1;
position: relative;
.crop-mask-block {
background-color: rgba(51, 51, 51, 0.8);
z-index: 2;
position: fixed;
box-sizing: border-box;
pointer-events: none;
}
.crop-circle-box {
position: fixed;
box-sizing: border-box;
z-index: 2;
pointer-events: none;
overflow: hidden;
.crop-circle {
width: 100%;
height: 100%;
}
}
.crop-image {
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
display: block !important;
backface-visibility: hidden;
}
.crop-border {
position: fixed;
border: 1px solid #fff;
box-sizing: border-box;
z-index: 3;
pointer-events: none;
}
.crop-grid {
position: fixed;
z-index: 3;
border-style: dashed;
border-color: #fff;
pointer-events: none;
opacity: 0.5;
}
.crop-angle {
position: fixed;
z-index: 3;
border-style: solid;
border-color: #fff;
pointer-events: none;
}
}
.fixed-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
flex-direction: row;
background-color: $uni-bg-color-grey;
.action-bar {
position: absolute;
top: -90rpx;
left: 10rpx;
display: flex;
.rotate-icon {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=");
background-size: 60% 60%;
background-repeat: no-repeat;
background-position: center;
width: 80rpx;
height: 80rpx;
&.is-reverse {
transform: rotateY(180deg);
}
}
}
.rechoose {
color: $uni-color-primary;
padding: 0 $uni-spacing-row-lg;
line-height: 100rpx;
}
.choose-btn {
color: $uni-color-primary;
text-align: center;
line-height: 100rpx;
flex: 1;
}
.button {
margin: auto $uni-spacing-row-lg auto auto;
background-color: $uni-color-primary;
color: #fff;
}
}
.safe-area-inset-bottom {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom); // IOS<11.2
padding-bottom: env(safe-area-inset-bottom); // IOS>=11.2
}
}
</style>

View File

@ -0,0 +1,604 @@
/**
* 图片编辑器-手势监听
* 1. wxs 暂不支持 es6 语法
* 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上uni-app 2.2.5及以上版本)
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c);
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
var o = (img.height - img.width) / 2; // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, img.height < area.height),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, img.width < area.width)
}
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
}
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
offset.x += e.x || 0;
offset.y += e.y || 0;
var image = e.instance.selectComponent('.crop-image');
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// image.setStyle({
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
// });
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
image.setStyle({
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
});
e.instance.callMethod('dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// 变更蒙版样式
var masks = e.instance.selectAllComponents('.crop-mask-block');
var maskStyles = [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
];
var len = masks.length;
for (var i = 0; i < len; i++) {
masks[i].setStyle(maskStyles[i]);
}
// 变更边框样式
if(area.showBorder) {
var border = e.instance.selectComponent('.crop-border');
border.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
var grids = e.instance.selectAllComponents('.crop-grid');
var gridStyles = [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
];
var len = grids.length;
for (var i = 0; i < len; i++) {
grids[i].setStyle(gridStyles[i]);
}
}
// 变更四个伸缩角样式
if(area.showAngle) {
var angles = e.instance.selectAllComponents('.crop-angle');
var angleStyles = [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
];
var len = angles.length;
for (var i = 0; i < len; i++) {
angles[i].setStyle(angleStyles[i]);
}
}
// 变更圆角样式
if(area.radius > 0) {
var circleBox = e.instance.selectComponent('.crop-circle-box');
var circle = e.instance.selectComponent('.crop-circle');
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
circleBox.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
circle.setStyle({
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = img.oldWidth * scale;
img.height = img.oldHeight * scale;
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = (e.x - offset.x) * (1 - scale / last);
e.y = (e.y - offset.y) * (1 - scale / last);
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=无1=左上2=右上3=左下4=右下;
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
if(y >= area.top - o && y <= area.top + area.angleSize + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
function rotateImage(e, o, r) {
rotate = (rotate + r) % 360;
if(img.minScale >= 1) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = (area.width / img.oldHeight)
}
if(minScale !== 1) {
scaleImage({
instance: o,
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: o,
check: true,
x: -ox - oy,
y: -oy + ox
});
};
module.exports = {
/**
* 初始化:观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: function(newVal, oldVal, o, i) {
if(newVal) {
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
img.src && changeImageRect({
instance: o,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance: o
});
// console.log('initRect', JSON.stringify(newVal))
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
if(!img.src) return;
scaleImage({
instance: o,
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
x: e.touches[0].pageX,
y: e.touches[0].pageY
});
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = e.touches;
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', JSON.stringify(e), activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: function(e, o) {
if(!img.src) return;
// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = area.width * (1 - area.minScale);
var maxY = area.height * (1 - area.minScale);
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
switch(activeAngle) {
case 1: // 左上角
x += areaOffset.left;
y += areaOffset.top;
if(x >= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, offset.x - area.left)
: false;
if(x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x += areaOffset.right;
y += areaOffset.top;
if(x <= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, area.right - offset.x - img.width)
: false;
if(-x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = -y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += areaOffset.left;
y += areaOffset.bottom;
if(x >= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, offset.x - area.left)
: false;
if(x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = -y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x += areaOffset.right;
y += areaOffset.bottom;
if(x <= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, area.right - offset.x - img.width)
: false;
if(-x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: o,
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: o,
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: o,
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: o,
});
scaleImage({
instance: o,
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: o,
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: function(e, o) {
rotateImage(e, o, 90);
},
rotateImage90: function(e, o) {
rotateImage(e, o, 90)
},
rotateImage270: function(e, o) {
rotateImage(e, o, 270)
},
// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
imageStyles: '',
maskStylesList: ['', '', '', ''],
borderStyles: '',
gridStylesList: ['', '', '', ''],
angleStylesList: ['', '', '', ''],
circleBoxStyles: '',
circleStyles: '',
}

View File

@ -0,0 +1,81 @@
{
"id": "qf-image-cropper",
"displayName": "图片裁剪插件",
"version": "2.2.4",
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
"keywords": [
"qf-image-cropper",
"图片裁剪",
"图片编辑",
"头像裁剪",
"小程序"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "u",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

View File

@ -0,0 +1,95 @@
# qf-image-cropper
## 图片裁剪插件
uniapp微信小程序图片裁剪插件支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
### 平台支持:
1. 支持微信小程序移动端、PC端、开发者工具
2. 支持H5平台2.1.0版本起)
3. 支持APP平台2.1.5版本起Android、IOS
4. 其他平台暂未测试兼容性未知
### 支持功能:
1. 自定义裁剪尺寸
2. 定点等比例缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
5. 裁剪生成新图片
6. 本地选择图片
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
8. 裁剪圆角图片:圆形、圆角矩形
### 属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| src | String | | 图片资源地址 |
| width | Number | 300 | 裁剪宽度 |
| height | Number | 300 | 裁剪高度 |
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
| minScale | Number | 1 | 图片最小缩放倍数 |
| maxScale | Number | 5 | 图片最大缩放倍数 |
| checkRange | Boolean | true | 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 |
| backgroundColor | String | | 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性则生成图片存在一定的透明块 |
| bounce | Boolean | true | 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 |
| rotatable | Boolean | true | 是否支持翻转 |
| reverseRotatable | Boolean | false | 是否支持逆向翻转 |
| choosable | Boolean | true | 是否支持从本地选择素材 |
| gpu | Boolean | false | 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 |
| angleSize | Number | 20 | 四个角尺寸单位px |
| angleBorderWidth | Number | 2 | 四个角边框宽度单位px |
| zIndex | Number/String | | 调整组件层级 |
| radius | Number | | 裁剪图片圆角半径单位px |
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
| delay | Number | 1000 | 图片从绘制到生成所需时间单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的 |
| @crop | EventHandle | | 剪裁完成后触发event = { tempFilePath }。在H5平台下tempFilePath 为 base64 |
### 基本用法
```
<template>
<div>
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
</div>
</template>
<script>
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
export default {
components: {
QfImageCropper
},
methods: {
handleCrop(e) {
uni.previewImage({
urls: [e.tempFilePath],
current: 0
});
}
}
}
</script>
```
通过ref组件实例可在进入页面后直接打开相册选择图片
```
mounted() {
this.$refs.qfImageCropper.chooseImage({ sourceType: ['album'] });
}
```
### 使用说明
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
```
{
"enablePullDownRefresh": false,
"disableScroll": true
}
```
2.建议使用本插件不要设置过大宽高的目标图片尺寸建议1365x1365以内否则可能会导致如下问题
```
1.界面卡顿,内存占用过高
2.生成图片失真(模糊)
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
```
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
4.src属性设置网络图片时图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。

451
pagesb/flashSale/index.vue Normal file
View File

@ -0,0 +1,451 @@
<template>
<view class="invoice-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<!-- 彩带背景 -->
<view class="seckill-bg">
<!-- 彩带 -->
<view class="confetti"></view>
<view class="confetti"></view>
<view class="confetti"></view>
<view class="confetti"></view>
<view class="confetti"></view>
<view class="confetti"></view>
<!-- 光斑 -->
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
<!-- 烟雾 -->
<view class="smoke"></view>
<view class="smoke"></view>
</view>
<view class="content">
<view class="title" style="transform: scaleY(1.5);height: 100rpx;display: flex;justify-content: center;color: #ffffff;font-size: 68rpx;font-weight: bolder;font-style: italic;letter-spacing: 4rpx;margin-bottom: 40rpx;">
{{ $t('common.FlashSale') }}
</view>
<view class="info">
<view class="cityBox">
<view>{{$t('common.SwitchRegion')}}</view>
<view>
<myDropdown v-model="state.city" :items="state.cityList" @change="changeCity"></myDropdown>
</view>
</view>
</view>
<view class="siteListBox">
<view class="top">
<view>
{{$t('common.Countdown')}}
</view>
<view>
{{ countDown?.formatted?.value }}
</view>
</view>
<view class="siteList">
<view class="ul" v-for="(item,index) in state.dataList" :key="index" @click="goOrder(item)">
<view class="left">
<view class="tags">
{{ $t("discount",{discount:item.discount}) }}
</view>
<view class="name">
{{item.siteName}}
</view>
<view class="type">
<text>
{{item.unitTypeName}} &nbsp;
</text>
<text>
[ {{ item.lockerName }} ]
</text>
</view>
<view class="volume">
<text>
{{$t('site.ReferenceVolume')}}{{item.volume}}
</text>
</view>
</view>
<view class="right">
<view class="priceBox">
<view class="price1">
{{currency}} {{item.flashSalePrice}}
</view>
<view class="price2">
{{currency}} {{item.price}}
</view>
</view>
<view class="icon">
<uv-icon name="arrow-right" size="14" color="#000000"></uv-icon>
</view>
</view>
</view>
<view class="ul" v-if="state.dataList.length==0">
<view style="text-align: center;width: 100%;font-size: 32rpx;padding: 10rpx;">
{{ $t('common.noData') }}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
//
import { useMainStore } from "@/store/index.js";
import navBar from "@/components/navBar.vue";
import flashSaleApi from "@/pagesb/Apis/flashSale.js";
import { onLoad,onShow } from '@dcloudio/uni-app';
import { getDistance} from '@/utils/common.js';
import myDropdown from "@/components/my-dropdown.vue";
import { useCountDown } from "@/hooks/useCountDown.js";
import { reactive, ref } from "vue";
import { currency } from '@/config/index.js'
import { useLocation } from "@/hooks/useLocation";
const { themeInfo } = useMainStore();
const { locationState,getLocation } = useLocation();
const state = reactive({
goodsIds: [],
orderId: "",
dataList: [],
goodsListRemark: "",
cityList: [],
startTime:null,
endTime:null,
allDataList: [],
});
onShow(() => {
getDataList()
})
const changeCity = () => {
applyCityFilter()
// startTime/endTime
if (state.dataList.length > 0 && state.startTime && state.endTime) {
initCountDown()
}
}
// city
const applyCityFilter = () => {
if (!state.city) {
state.dataList = []
return
}
state.dataList = state.allDataList.filter(x => x.district === state.city)
}
// district
const buildCityListFromAllData = () => {
const districts = Array.from(
new Set((state.allDataList || []).map(x => x.district).filter(Boolean))
)
state.cityList = districts.map(d => ({ label: d, value: d }))
// city
if (!state.city || !districts.includes(state.city)) {
state.city = districts[0] || ""
}
}
const goOrder = (item) => {
uni.navigateTo({
url: `/pages/setOrder/index?id=${item.id}`,
});
}
const getCityList = () => {
uni.showLoading()
// let data = {}
// if (locationState?.latitude && locationState?.longitude) {
// data = {
// latitude: locationState.latitude,
// longitude: locationState.longitude
// }
// }
flashSaleApi.GetFlashSaleDistrict().then(res => {
state.cityList = []
if (res.code == 200) {
state.cityList = res.data.map(item =>({label:item,value:item}));
if(state.cityList.length>0){
state.city = state.cityList[0].value
getDataList()
}
}
}).finally(() => {
uni.hideLoading()
})
}
const getDataList = () => {
uni.showLoading()
state.dataList = []
flashSaleApi.GetFlashSaleInfo().then(res => {
if (res.code == 200) {
state.startTime = res.data.flashSaleStartTime
state.endTime = res.data.flashSaleEndTime
const lockerData = res.data.lockerData;
//
if (locationState?.latitude && locationState?.longitude) {
const { latitude, longitude } = locationState;
lockerData.forEach(item => {
const { distance, number } = getDistance(latitude, longitude, item.latitude, item.longitude);
item.distance = distance;
item.distanceNumber = number;
});
lockerData.sort((a, b) => a.distanceNumber - b.distanceNumber);
}
state.dataList= lockerData
//
state.allDataList = lockerData || []
// district cityList
buildCityListFromAllData()
// city
applyCityFilter()
if(state.dataList.length>0 && (state.startTime && state.endTime)){
initCountDown()
}
}
}).finally(() => {
uni.hideLoading()
})
}
let countDown = null
const initCountDown = () => {
if (!countDown) {
countDown = useCountDown(
state.startTime,
state.endTime,
() => {
getDataList()
uni.showToast({
title: '倒计时结束',
icon: 'none'
})
}
)
} else {
//
countDown.reset(state.startTime, state.endTime)
}
countDown.start()
}
onLoad((option) => {
state.orderId = option.orderId;
getLocation().finally(() => {
getDataList();
});
})
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
.invoice-wrap {
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
padding-bottom: 20rpx;
background: linear-gradient(to bottom,
var(--right-linear),
var(--left-linear2));
.content {
width: 100%;
z-index: 2;
.info {
.cityBox {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
font-weight: bold;
border-radius: 16rpx;
background-color: #FFFFFF;
}
}
.siteListBox {
margin-top: 20rpx;
margin-bottom: 200rpx;
.top {
border-radius: 16rpx 16rpx 0 0;
display: flex;
justify-content: space-between;
padding: 20rpx;
background-color: #12324D;
color: #ffffff;
font-size: 42rpx;
font-weight: bold;
letter-spacing: 4rpx;
}
.siteList {
background-color: #FFFFFF;
border-radius: 0 0 16rpx 16rpx;
font-size: 26rpx;
font-weight: bold;
.ul {
padding: 20rpx;
border-bottom: 1px dashed var(--main-color);
display: flex;
justify-content: space-between;
align-items: center;
&:last-child{
border: none;
}
.left {
.tags{
text-align: center;
padding: 6rpx 10rpx;
width: fit-content;
color: #FFFFFF;
background-color: rgb(252, 89, 116);
font-size: 22rpx;
border-radius: 8rpx;
}
.name {
margin-top: 4rpx;
font-size: 32rpx;
font-weight: bold;
}
.type {
margin-top: 4rpx;
text {
display: inline-block;
}
}
.volume {
margin-top: 4rpx;
text {
display: inline-block;
}
}
}
.right {
display: flex;
align-items: center;
.priceBox {
display: flex;
align-items: center;
flex-direction: column;
font-style: italic;
.price1 {
font-size: 52rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
.price2 {
font-size: 52rpx;
font-weight: bold;
color: #a8a8a8;
position: relative;
&:after {
content: " ";
display: block;
width: 120%;
height: 2px;
background-color: var(--btn-color3);
// transform: rotate(9deg);
position: absolute;
top: 49%;
left: -5px;
}
}
}
.icon {
margin-left: 20rpx;
display: flex;
align-items: center;
}
}
}
}
}
}
}
.seckill-bg {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 0;
// background-color: #ff4b2b; /* */
}
/* 彩带,限制在顶部 300px 内 */
.confetti {
position: absolute;
width: 8px;
height: 20px;
border-radius: 2px;
top: 0; /* 起点在顶部 */
animation: fall 4s linear infinite;
transform-origin: center;
}
.confetti:nth-child(1) { left: 5%; animation-delay: 0s; animation-duration: 4s; background: #ffeb3b; }
.confetti:nth-child(2) { left: 18%; animation-delay: 0.5s; animation-duration: 3.5s; background: #00e5ff; }
.confetti:nth-child(3) { left: 32%; animation-delay: 1s; animation-duration: 4.2s; background: #ff4081; }
.confetti:nth-child(4) { left: 46%; animation-delay: 1.2s; animation-duration: 3.8s; background: #76ff03; }
.confetti:nth-child(5) { left: 60%; animation-delay: 1.5s; animation-duration: 4.5s; background: #ff6e40; }
.confetti:nth-child(6) { left: 75%; animation-delay: 0.3s; animation-duration: 4.1s; background: #e040fb; }
.confetti:nth-child(7) { left: 88%; animation-delay: 0.7s; animation-duration: 3.7s; background: #ffea00; }
@keyframes fall {
0% { transform: translate(0,0) rotate(0deg) scale(0.8); opacity: 1; }
50% { transform: translate(20px,150px) rotate(180deg) scale(1.2); opacity: 0.9; }
100% { transform: translate(-20px,300px) rotate(360deg) scale(0.8); opacity: 0; }
}
/* 光斑 */
.dot {
position: absolute;
width: 10px;
height: 10px;
background: rgba(255,255,255,0.7);
border-radius: 50%;
animation: blink 3s infinite;
}
.dot:nth-child(7) { top: 10%; left: 20%; animation-delay: 0s;}
.dot:nth-child(8) { top: 16%; left: 60%; animation-delay: 1s;}
.dot:nth-child(9) { top: 13%; left: 80%; animation-delay: 2s;}
@keyframes blink {
0%,100% {opacity: 0.2;}
50% {opacity: 1;}
}
/* 烟雾 */
.smoke {
position: absolute;
width: 200px;
height: 200px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.08), transparent 70%);
top: 70%;
animation: smokeDrift 15s linear infinite;
filter: blur(20px);
opacity: 0.5;
}
.smoke:nth-child(10) { left: 10%; animation-duration: 18s; width: 250px; height: 250px; }
.smoke:nth-child(11) { left: 60%; animation-duration: 20s; width: 300px; height: 300px; }
@keyframes smokeDrift {
0% { transform: translateX(0) translateY(0) scale(0.5);}
50% { transform: translateX(100px) translateY(-50vh) scale(1);}
100% { transform: translateX(200px) translateY(-100vh) scale(1.5);}
}
</style>

340
pagesb/houseKey/index.vue Normal file
View File

@ -0,0 +1,340 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<navBar class="navBar"></navBar>
<view class="wrapbox">
<view class="container-form" v-for="(item,index) in state.dataList" :key="item.authorizeId || index">
<view class="cHeader">
</view>
<view class="content">
<uv-form labelWidth="auto">
<uv-form-item :label="$t('houseKey.FriendsName')" prop="formData.name">
<uv-input shape="circle" border="none" placeholder="* * *" v-model="item.userName">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('houseKey.AuthorizationDate')" prop="formData.date" @click="showCalendar(item)">
<uv-input shape="circle" border="none" :placeholder="$t('houseKey.date')" readonly v-model="item.endDate" >
</uv-input>
</uv-form-item>
<view class="past-date" v-if="getIsPast(item)">{{ $t('houseKey.overdue') }}</view>
<uv-form-item :label="$t('houseKey.ReceiveNotifications')" prop="formData.isNotice">
<uv-input shape="circle" disabled disabledColor="#ffffff" :placeholder="$t('houseKey.getNote')" border="none">
</uv-input>
<template v-slot:right>
<uv-switch v-model="item.isNotice" size="20"></uv-switch>
</template>
</uv-form-item>
<uv-form-item :label="$t('houseKey.PhoneNumber')" prop="formData.phone">
<uv-input shape="circle" border="none" :placeholder="$t('houseKey.otherPhone')" v-model="item.phone">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('houseKey.email')" prop="formData.email">
<uv-input shape="circle" border="none" :placeholder="$t('houseKey.otherEmail')" v-model="item.email">
</uv-input>
</uv-form-item>
</uv-form>
</view>
<view class="footer">
<view class="left">
<button @click="add(item)" class="btn">{{ $t('common.update') }}</button>
<button class="btn" @click="deleteItem(item)">{{ $t('common.delete') }}</button>
</view>
</view>
</view>
<view class="container-form">
<view class="cHeader">
</view>
<view class="content">
<uv-form labelWidth="auto">
<uv-form-item :label="$t('houseKey.FriendsName')" prop="formData.name">
<uv-input shape="circle" border="none" placeholder="* * *" v-model="state.formData.userName">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('houseKey.AuthorizationDate')" prop="formData.date" @click="showCalendar()">
<uv-input shape="circle" border="none" :placeholder="$t('houseKey.date')" v-model="state.formData.endDate" readonly >
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('houseKey.ReceiveNotifications')" prop="formData.isNotice">
<uv-input shape="circle" disabled disabledColor="#ffffff" :placeholder="$t('houseKey.getNote')" border="none">
</uv-input>
<template v-slot:right>
<uv-switch v-model="state.formData.isNotice" size="20"></uv-switch>
</template>
</uv-form-item>
<uv-form-item :label="$t('houseKey.PhoneNumber')" prop="formData.phone">
<uv-input shape="circle" border="none" :placeholder="$t('houseKey.otherPhone')" v-model="state.formData.phone">
</uv-input>
</uv-form-item>
<uv-form-item :label="$t('houseKey.email')" prop="formData.email">
<uv-input shape="circle" border="none" :placeholder="$t('houseKey.otherEmail')" v-model="state.formData.email">
</uv-input>
</uv-form-item>
</uv-form>
</view>
<view class="footer">
<view class="left">
<button @click="add(null)" class="btn">{{ $t('houseKey.AddAuthorization') }}</button>
<!-- <button class="btn" @click="makePhoneCall">兑换</button> -->
</view>
</view>
</view>
</view>
</view>
<uv-calendars ref="calendar" @confirm="confirm"></uv-calendars>
</template>
<script setup>
import navBar from "@/components/navBar.vue";
import { useLockApi } from '@/Apis/lock.js';
import { ref } from "vue";
import { onLoad,onShow } from "@dcloudio/uni-app";
import dayjs from "dayjs";
//
import { useMainStore } from "@/store/index.js";
//
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { themeInfo,storeState } = useMainStore();
const getApi = useLockApi();
const calendar = ref();
const state = ref({
inviteShow: false,
orderId: null,
dataList: [],
item:null,
formData:{
userName:'',
endDate:'',
phone:'',
email:'',
isNotice:false
}
});
const showCalendar = (item)=>{
state.value.item = item || null;
calendar.value.open();
}
const confirm = (e) => {
if(state.value.item){
state.value.item.endDate = dayjs(e.fulldate).format("YYYY/MM/DD");
}else{
state.value.formData.endDate = dayjs(e.fulldate).format("YYYY/MM/DD")
}
};
const getIsPast = (item) => {
return dayjs(item.authorDate).isBefore(dayjs().format("YYYY/MM/DD"));
};
onLoad((params) => {
state.value.orderId = params.Order_ID;
if(!state.value.orderId){
uni.showToast({
title: "参数错误Params error",
icon: "none",
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}else{
getDataList();
}
});
onShow(() => {});
const getDataList = () => {
getApi.GetOrderAuthorizeList({orderId:state.value.orderId}).then(res => {
if (res.code == 200) {
state.value.dataList = res.data;
state.value.dataList.forEach(item => item.authorDate = item.endDate);
}
})
}
const deleteItem = (item) => {
uni.showModal({
title: "提示",
content: "确定删除吗(Delete It)",
success: (res) => {
if (res.confirm) {
getApi.DeleteOrderAuthorize({authorizeId:item.authorizeId}).then(res => {
if (res.code == 200) {
uni.showToast({
title: "删除成功(Delete Successful)",
icon: "none",
});
getDataList();
}
})
}
},
});
}
const add = (item) => {
let formData = state.value.formData;
if(item){
formData = item;
}
formData.orderId = state.value.orderId;
if(!formData.userName){
uni.showToast({
title: t('houseKey.EnterFriendsName'),
icon: "none",
});
return;
}
if(!formData.endDate){
uni.showToast({
title: t('houseKey.EnterAuthorizationDate'),
icon: "none",
});
return;
}
if(!formData.phone){
//#ifdef MP-WEIXIN
uni.showToast({
title: t('houseKey.EnterPhoneNumber'),
icon: "none",
});
//#endif
//#ifdef H5
uni.showToast({
title: t('houseKey.EnterEmail'),
icon: "none",
});
//#endif
return;
}
if(formData.phone == storeState.userInfo.phone){
uni.showToast({
title: t('houseKey.CannotAuthorizeYourself'),
icon: "none",
});
return;
}
if(formData.authorizeId){
formData.id = formData.authorizeId;
getApi.UpdateOrderAuthorizeCustomer(formData).then(res => {
if (res.code == 200) {
uni.showToast({
title: t('houseKey.UpdateSuccessful'),
icon: "none",
});
getDataList();
}
})
}else{
getApi.OrderAuthorizeCustomer(formData).then(res => {
if (res.code == 200) {
state.value.formData = {
userName:'',
endDate:'',
phone:'',
email:'',
isNotice:false
}
uni.showToast({
title: t('houseKey.AddedSuccessfully'),
icon: "none",
});
getDataList();
}
})
}
}
// onShareAppMessage((res) => {
// if (res.from === "button") {
// }
// return shareParam;
// });
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
uni-page-body {
height: 100%;
background: linear-gradient(0deg, rgb(1, 169, 188), rgb(10, 132, 184));
overflow: auto;
}
.navBar {
position: relative;
}
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: start;
width: 100%;
overflow-x: hidden;
background: linear-gradient(0deg, var(--left-linear), var(--right-linear));
.wrapbox {
padding: 20rpx;
height: 100%;
width: 100%;
background: white;
}
}
.container-form {
width: 100%;
box-sizing: border-box;
background: #242a37;
border-radius: 18rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 20rpx;
font-size: 24rpx;
margin-bottom: 20rpx;
.content {
padding: 0 20rpx;
padding-top: 20rpx;
width: 100%;
:deep(.uv-form-item){
background-color: #FFF;
border-radius: 99px;
padding:0 8px;
margin-bottom: 20rpx;
.uv-form-item__body{
padding: 10rpx 0;
}
}
.past-date {
margin: 5px 0 8px;
color: #ff1d0c;
}
}
.footer {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
padding: 10rpx 0;
padding-top: 20rpx;
font-weight: bold;
.left {
display: flex;
justify-content: space-between;
font-size: 22rpx;
padding: 0 20rpx;
width: 100%;
.btn {
width: 49%;
height: 70rpx;
line-height: 70rpx;
padding: 0;
font-size: 32rpx;
border-radius: 999px;
border: 1rpx solid transparent;
&:nth-child(2) {
background-color: #ff1d0c;
color: #FFF;
}
&:nth-child(1) {
background-color: var(--main-color);
}
}
}
.right {
font-size: 20rpx;
}
}
}
</style>

728
pagesb/initLock/index.vue Normal file
View File

@ -0,0 +1,728 @@
<template>
<view class="init-lock-wrap" :class="[`${themeInfo.theme}-theme`]">
<NavBar></NavBar>
<view class="search" @click="showScanPopup()">
<uv-icon name="search" size="20" color="#c0bdc0"></uv-icon>
</view>
<view class="lock-info" v-if="state.lockInfo.lockData">
<view class="name">{{ state.lockInfo.deviceName || state.lockInfo.MAC }}</view>
<view class="lock-wrap">
<uv-loading-icon mode="spinner" size="40" :color="themeInfo.activeColor" v-if="state.initLoading"></uv-loading-icon>
<uv-icon name="lock-fill" size="70" :color="themeInfo.activeColor" v-else></uv-icon>
</view>
<view class="tip">该智能锁已初始化成功</view>
<view class="setting-wrap">
<view class="wifi" @click="openNetworkPopup">
<uv-icon name="setting" size="20" :color="themeInfo.activeColor"></uv-icon>
<view class="text">WiFi</view>
</view>
</view>
</view>
<view class="empty-lock" v-else>
<view class="empty" @click="showScanPopup(true)">
<uv-icon name="plus" size="36" color="#c0bdc0"></uv-icon>
</view>
<view class="text">添加锁时手机必须在锁旁边</view>
</view>
<!-- 搜索智能锁弹框 -->
<uv-popup ref="scanPopup" class="scan-popup" mode="bottom" closeable :close-on-click-overlay="false">
<view class="scan-wrap">
<template v-if="state.initLockList?.length">
<view class="name">成功初始化的智能锁列表</view>
<view class="item-wrap" @click="selectLock(item)">
<div class="item active" v-for="item in state.initLockList" :key="item.lockId">
<text>{{ item.deviceName || item.MAC }}</text>
<uv-icon v-if="item.lockId == state.lockInfo.lockId" name="checkmark-circle" size="14" :color="themeInfo.activeColor"></uv-icon>
</div>
</view>
</template>
<view class="btn-wrap">
<view class="btn" @click="startScan">开始扫描</view>
<view class="btn">停止扫描</view>
</view>
<view class="loading-wrap" v-if="state.searchLoading">
<uv-loading-icon mode="spinner" vertical text="扫描附近的智能锁设备中"></uv-loading-icon>
</view>
<template v-else-if="state.searchList?.length">
<view class="name">扫描的智能锁列表</view>
<view class="item-wrap">
<div class="item" v-for="item in state.searchList" :key="item.deviceId" @click="handleSetName(item)" :class="{ 'active': item.isSettingMode }">
<text>{{ item.deviceName || item.MAC }}</text>
<uv-icon v-if="item.isSettingMode" name="plus-circle" size="14" :color="themeInfo.activeColor"></uv-icon>
</div>
</view>
</template>
<view class="empty-wrap" v-else>
<uv-icon name="empty-data" size="26" color="#b3b4b5"></uv-icon>
<text class="text">智能锁为空~</text>
</view>
</view>
</uv-popup>
<!-- 修改名称 -->
<uv-modal
ref="nameModal"
title="修改名称"
showCancelButton
:closeOnClickOverlay="false"
confirmText="确认初始化"
:confirmColor="themeInfo.activeColor"
@confirm="handleInitLock">
<uv-input v-model="state.lockAlias" placeholder="请输入名称" border="surround"></uv-input>
</uv-modal>
<!-- 配置网络 -->
<uv-popup ref="networkPopup" mode="bottom" :close-on-click-overlay="false">
<view class="network-wrap">
<view class="nav-wrap">
<uv-icon name="arrow-leftward" :color="themeInfo.iconColor" @click="closeNetwork"></uv-icon>
<view class="name">配置网络</view>
</view>
<view class="tip">不支持 5G WiFi 网络请选择 2.4G WiFi 网络进行配置</view>
<view class="input-wrap" @click="openWifiPopup">
<view class="label">WiFi 名称</view>
<view class="name">{{ wifiData.selectWifi.SSID }}</view>
<uv-icon name="arrow-right"></uv-icon>
</view>
<view class="input-wrap">
<view class="label">WiFi 密码</view>
<uv-input v-model="wifiData.password" placeholder="请输入WiFi密码" border="none"></uv-input>
</view>
<view class="btn" @click="handleConfigWifi">确定</view>
</view>
</uv-popup>
<uv-popup ref="wifiPopup" mode="bottom" :close-on-click-overlay="false">
<view class="wifi-wrap">
<view class="nav-wrap">
<text style="z-index: 9;" @click="closeWifiPopup">取消</text>
<view class="name">选择网络</view>
<text style="z-index: 9;" @click="handleSearchWifi">搜索</text>
</view>
<view class="loading-wrap" v-if="wifiData.isLoading">
<uv-loading-icon mode="spinner" vertical text="正在扫描智能锁附近可用wifi列表"></uv-loading-icon>
</view>
<template v-if="wifiData.list?.length">
<view class="wifi-item" v-for="(item, index) in wifiData.list" :key="index" @click="selectWifi(item)">
{{ item.SSID }}
</view>
</template>
</view>
</uv-popup>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import CryptoJs from "crypto-js";
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import { useLockApi } from "/Apis/lock.js";
import { ttLockRequest, ttLockRequest2 } from "./lockInitApi.js";
import NavBar from "@/components/navBar.vue";
const lockApi = useLockApi();
const scanPopup = ref();
const nameModal = ref();
const wifiPopup = ref();
const networkPopup = ref();
const state = reactive({
searchLoading: false, //
initLoading: false, //
lockInfo: {}, //
initInfo: {}, //
initLockList: [],
searchList: [],
ttToken: "",
lockAlias: ""
});
const getLockList = async () => {
const openId = uni.getStorageSync("openId");
const { code, data } = await lockApi.GetInitLockList({ openId });
if (code == 200) {
state.initLockList = data;
if (state.initLockList?.length) {
state.lockInfo = state.initLockList[0];
}
}
}
const selectLock = (item) => {
state.lockInfo = item;
}
onMounted(() => {
getLockList();
});
const showScanPopup = (check) => {
if (check && state.lockInfo.lockData) return;
scanPopup.value.open();
startScan();
}
const checkBlueTooth = () => {
return new Promise(resolve => {
uni.openBluetoothAdapter({
success() {
resolve(true);
},
fail(err) {
console.log(err);
uni.showToast({
title: `蓝牙初始化失败:${err.errCode === 10001 ? "请打开蓝牙" : err.errMsg}`,
icon: 'none'
});
resolve(false);
}
});
});
}
//
const startScan = async () => {
const openBlue = await checkBlueTooth();
if (!openBlue || state.searchLoading) return;
state.searchLoading = true;
requirePlugin("ttPlugin", ({ startScanBleDevice }) => {
//
startScanBleDevice((lockDevice, lockList) => {
// TODO
state.searchList = lockList;
state.searchLoading = false;
stopScan();
}, (err) => {
console.error(err);
uni.showToast({
title: `蓝牙扫描智能锁设备失败:${err.errorMsg}`,
icon: 'none'
});
state.searchLoading = false;
}
);
});
};
//
const stopScan = () => {
requirePlugin("ttPlugin", ({ stopScanBleDevice }) => {
stopScanBleDevice().then(res => {
state.searchLoading = false;
});
});
}
// token
const getLoginToken = () => {
return new Promise(resolve => {
if (state.ttToken) {
resolve(true);
return;
}
ttLockRequest("/oauth2/token", "POST", {
"client_id": "7946f0d923934a61baefb3303de4d132",
"client_secret": "56d9721abbc3d22a58452c24131a5554",
"grant_type": "password",
"redirect_uri": "http://www.sciener.cn",
"username": "+8617727694079",
"password": CryptoJs.MD5(String("lqx123456")).toString()
}).then(res => {
state.ttToken = res.access_token;
if (state.ttToken) {
uni.setStorageSync("tt_access_token", state.ttToken);
resolve(true);
} else {
resolve(false);
}
});
});
}
const handleSetName = (deviceFromScan) => {
if (!deviceFromScan.isSettingMode) {
uni.showToast({
title: `智能锁${deviceFromScan.deviceName || deviceFromScan.MAC}已被初始化,当前不可添加`,
icon: 'none'
});
return;
}
state.initInfo = deviceFromScan;
nameModal.value.open();
}
//
const handleInitLock = () => {
nameModal.value.close();
let deviceFromScan = state.initInfo;
if (state.initLoading) return;
state.initLoading = true;
uni.showLoading({
title: '正在初始化智能锁'
});
requirePlugin("ttPlugin", ({ getLockVersion, initLock }) => {
//
getLockVersion({ deviceFromScan }).then(res => {
if (res.errorCode == 0) {
initLock({ deviceFromScan }).then(async result => {
uni.hideLoading();
state.initLoading = false;
if (result.errorCode == 0) {
console.log('----智能锁数据----', result);
const res = await getLoginToken();
if (!res) {
uni.showToast({
title: "获取tt用户token失败正在重置智能锁",
icon: 'none'
});
handleResetLock(result.lockData);
return;
}
ttLockRequest2("/v3/lock/initialize", "POST", {
"lockData": result.lockData,
"lockAlias": state.lockAlias
}).then(res => {
console.log('------tt用户同步数据------', res);
if (res.lockId) {
uni.showToast({
title: "智能锁已成功添加",
icon: 'none'
});
state.lockInfo = {
lockId: String(res.lockId),
lockData: result.lockData,
deviceId: state.initInfo.deviceId,
deviceName: state.lockAlias || state.initInfo.deviceName,
}
saveLockData();
openNetworkPopup();
} else {
uni.showToast({
title: "智能锁数据上传失败, 正在重置智能锁",
icon: 'none'
});
handleResetLock(result.lockData);
}
});
scanPopup.value.close();
} else {
uni.showToast({
title: `初始化智能锁失败:${result.errorMsg}`,
icon: 'none'
});
}
});
} else {
uni.hideLoading();
state.initLoading = false;
uni.showToast({
title: `更新智能锁版本信息失败:${res.errorMsg}`,
icon: 'none'
});
};
});
});
}
//
const saveLockData = () => {
if (!state.lockInfo.lockData) return;
const openId = uni.getStorageSync("openId");
lockApi.SaveInitLock({
openId,
...state.lockInfo
});
}
//
const handleResetLock = (lockData) => {
requirePlugin("ttPlugin", ({ resetLock }) => {
resetLock({ lockData }).then(res => {
if (res.errorCode == 0) {
uni.showToast({
title: '智能锁重置成功',
icon: 'none'
});
} else {
uni.showToast({
title: `智能锁重置失败,请长按重置键进行设备重置:${res.errorMsg}`,
icon: 'none'
});
}
});
});
}
const wifiData = reactive({
password: "",
isLoading: false,
list: [],
selectWifi: {},
});
//
const openNetworkPopup = () => {
wifiData.password = "";
networkPopup.value.open();
}
//
const closeNetwork = () => {
networkPopup.value.close();
}
const openWifiPopup = () => {
wifiPopup.value.open();
if (!wifiData.list?.length) handleSearchWifi();
}
const closeWifiPopup = () => {
wifiPopup.value.close();
}
const handleSearchWifi = () => {
if (wifiData.isLoading) return;
wifiData.isLoading = true;
wifiData.list = [];
requirePlugin("ttPlugin", ({ scanWifi }) => {
scanWifi({ lockData: state.lockInfo.lockData }).then(res => {
wifiData.isLoading = false;
if (res.errorCode == 0) {
wifiData.list = res.data.wifiList;
uni.showToast({
title: "扫描智能锁附近可用wifi列表成功",
icon: 'none'
});
} else {
uni.showToast({
title: `扫描智能锁附近可用wifi列表失败${res.errorMsg}`,
icon: 'none'
});
}
})
});
}
const selectWifi = (item) => {
wifiData.selectWifi = item;
closeWifiPopup();
}
// wifi
// const getLockWifi = () => {
// ttLockRequest2("/v3/wifiLock/detail", "POST", {
// "lockId": state.lockInfo.lockId,
// }).then(res => {
// wifiData.wifiInfo = res;
// });
// }
// wifi
const handleConfigWifi = () => {
if (!wifiData.password) {
uni.showToast({
title: "请输入wifi密码",
icon: 'none'
});
return;
}
uni.showLoading({ title: "正在配置wifi信息" });
requirePlugin("ttPlugin", ({ configWifi, configServer }) => {
configWifi({
config: {
SSID: wifiData.selectWifi.SSID,
password: wifiData.password
},
lockData: state.lockInfo.lockData
}).then(res => {
if (res.errorCode == 0) {
//
configServer({
config: {
server: "cnwifilock.ttlock.com",
port: 4999,
},
lockData: state.lockInfo.lockData
}).then(async res => {
if (res.errorCode == 0) {
const res = await getLoginToken();
if (!res) {
uni.hideLoading();
uni.showToast({
title: "获取tt用户token失败",
icon: 'none'
});
return;
}
//
ttLockRequest2("/v3/wifiLock/updateNetwork", "POST", {
lockId: state.lockInfo.lockId, // ID
networkName: wifiData.selectWifi.SSID, //
rssi: wifiData.selectWifi.rssi, // Wifi
useStaticIp: false, // 使IP
}).then(result => {
console.log('---wifi上传到服务器的配置---', result);
uni.hideLoading();
if (result.errcode === 0) {
// getLockWifi();
uni.showToast({
title: "Wifi参数已上传服务器",
icon: 'none'
});
closeNetwork();
} else {
uni.showToast({
title: "Wifi参数上传服务器失败",
icon: 'none'
});
}
}).catch(err => {
console.log(err)
uni.hideLoading();
});
} else {
uni.showToast({
title: `配置服务器信息失败:${res.errorMsg}`,
icon: 'none'
});
uni.hideLoading();
}
});
} else {
uni.showToast({
title: `配置wifi信息失败${res.errorMsg}`,
icon: 'none'
});
uni.hideLoading();
}
})
});
}
</script>
<style lang="scss" scoped>
.init-lock-wrap {
position: relative;
min-height: 100vh;
background: #FFFFFF;
.search {
position: absolute;
right: 20px;
}
.lock-info {
display: flex;
flex-direction: column;
align-items: center;
.name {
margin-bottom: 80rpx;
color: #333333;
}
.lock-wrap {
display: flex;
justify-content: center;
align-items: center;
width: 400rpx;
height: 400rpx;
border-radius: 50%;
box-shadow: 0px 6px 10px rgba($color: #000000, $alpha: 0.2);
}
.tip {
margin-top: 40rpx;
}
.setting-wrap {
width: 100%;
margin-top: 60rpx;
padding-top: 60rpx;
border-top: 1px solid #f3f3ed;
.wifi {
display: flex;
align-items: center;
flex-direction: column;
width: 25%;
.text {
margin-top: 16rpx;
font-size: 24rpx;
}
}
}
}
.empty-lock {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 160rpx;
.empty {
display: flex;
justify-content: center;
align-items: center;
width: 400rpx;
height: 400rpx;
border-radius: 50%;
box-shadow: 0px 6px 10px rgba($color: #000000, $alpha: 0.2);
}
.text {
margin-top: 140rpx;
color: #656265;
font-size: 28rpx;
}
}
.scan-wrap {
padding: 60rpx 20rpx 0;
height: calc(100vh - 400rpx);
.btn-wrap {
display: flex;
margin: 30rpx 0;
.btn {
padding: 20rpx;
margin-right: 30rpx;
font-size: 28rpx;
font-weight: bold;
color: var(--text-color);
background: var(--active-color);
border-radius: 16rpx;
}
}
.name {
margin: 20rpx 0;
color: #333333;
}
.item-wrap {
.item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
padding: 0 32rpx;
color: #333333;
background: #F2F4F8;
border-bottom: 1px solid #E8E8E8;
&.active {
background: #F9FBD8;
}
}
}
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 200rpx;
.text {
margin-top: 10rpx;
text-align: center;
color: #b3b4b5;
font-size: 32rpx;
}
}
.loading-wrap {
margin-top: 200rpx;
}
}
.network-wrap {
height: calc(100vh - 400rpx);
background: #F6F4F7;
.nav-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
background: var(--main-color);
.name {
flex: 1;
text-align: center;
}
}
.tip {
padding: 20rpx 0 20rpx 20rpx;
font-size: 28rpx;
background: #F2F6Fc;
}
.input-wrap {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 20rpx;
font-size: 28rpx;
background: #FFFFFF;
border-bottom: 1px solid #F2F6Fc;
.name {
flex: 1;
text-align: right;
margin-right: 20rpx;
}
:deep(input) {
text-align: right !important;
}
}
.btn {
margin: 40rpx 80rpx 0;
padding: 20rpx 0;
border-radius: 6px;
text-align: center;
background: var(--main-color);
}
}
.wifi-wrap {
height: 600rpx;
overflow: auto;
background: #FFFFFF;
.nav-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
border-bottom: 1px solid #F2F6Fc;
.name {
flex: 1;
text-align: center;
}
}
.loading-wrap {
margin-top: 120rpx;
}
.wifi-item {
padding: 30rpx 0 30rpx 40rpx;
border-bottom: 1px solid #F2F6Fc;
}
}
}
</style>

View File

@ -0,0 +1,158 @@
const getResponseData = (response) => {
switch (response.type) {
case 0:
return response["data"]; // 操作成功
default: {
uni.showToast({
icon: "none",
title: response.errorMsg,
});
return null;
}
}
};
export const ttLockRequest = (url, method, params) => {
return new Promise(resolve => {
uni.request({
url: `https://cnapi.ttlock.com${url}`,
header: {
"content-type": "application/x-www-form-urlencoded",
},
data: params,
method: method,
success: (response) => {
let result = null;
switch (response.statusCode) {
case 200:
if (!!response.data && typeof response.data["errcode"] === "undefined") {
result = getResponseData({
data: response.data,
errorCode: -2,
errorMsg: "操作成功",
type: 0,
});
} else {
let errMsg = response.data["errcode"] === 0 ? "操作成功" : response.data["errmsg"];
result = getResponseData({
data: response.data,
errorCode: -2,
errorMsg: errMsg,
type: 0,
});
}
break;
default:
result = getResponseData({
errorCode: -2,
errorMsg: `服务器请求失败,状态码:${response.statusCode}`,
type: 2,
});
break;
}
resolve(result);
},
fail: (err) => {
let result = getResponseData({
errorCode: -1,
errorMsg: "服务器请求失败,请检查服务器域名是否已被列入白名单",
type: 3,
});
resolve(result);
},
});
});
};
export const ttLockRequest2 = (url, method, params) => {
return new Promise(resolve => {
uni.request({
url: `https://api.ttlock.com${url}`,
header: {
"content-type": "application/x-www-form-urlencoded",
},
data: _makeParams(params),
method: method,
dataType: "json",
success: (response) => {
let result = null;
switch (response.statusCode) {
case 200:
if (!!response.data && typeof response.data["errcode"] === "undefined") {
result = getResponseData({
data: response.data,
errorCode: -2,
errorMsg: "操作成功",
type: 0,
});
} else {
let errMsg = response.data["errcode"] === 0 ? "操作成功" : response.data["errmsg"];
result = getResponseData({
data: response.data,
errorCode: -2,
errorMsg: errMsg,
type: 0,
});
}
break;
default:
result = getResponseData({
errorCode: -2,
errorMsg: `服务器请求失败,状态码:${response.statusCode}`,
type: 2,
});
break;
}
resolve(result);
},
fail: (err) => {
let result = getResponseData({
errorCode: -1,
errorMsg: "服务器请求失败,请检查服务器域名是否已被列入白名单",
type: 3,
});
resolve(result);
},
});
});
};
function _generateParams(params) {
if (!params) return {};
for (let key of Object.keys(params)) {
if (params[key] === null) {
params[key] = undefined;
continue;
}
const type = typeof params[key];
switch (type) {
case "function":
{
params[key] = undefined;
}
break;
case "object":
{
params[key] = JSON.stringify(params[key]);
}
break;
case "number":
case "string":
case "boolean":
default:
break;
}
}
return JSON.parse(JSON.stringify(params));
}
function _makeParams(params) {
return JSON.parse(
JSON.stringify({
..._generateParams(params),
clientId: "7946f0d923934a61baefb3303de4d132",
accessToken: uni.getStorageSync("tt_access_token"),
date: Date.now(),
})
);
}

394
pagesb/invitation/index.vue Normal file
View File

@ -0,0 +1,394 @@
<template>
<view class="invitation-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<!-- <canvas type="2d" id="posterCanvas" :style="{ width: `${state.canvasWidth}px`, height: `${state.canvasHeight}px` }"></canvas> -->
<view class="QrCode" :style="{ height: `${state.canvasHeight}px` }">
<image class="qrcodeImg" :src="state.qrCodeBase64"></image>
</view>
<view class="bottom-wrap">
<view class="btn-wrap" @click="saveImageToAlbum(state.qrCodeBase64)">
<view class="icon-wrap">
<uv-icon name="download" size="30" color="#1B493E"></uv-icon>
</view>
<view class="text">{{ $t("referrerInfo.loadQrCode") }}</view>
</view>
<!-- <view class="btn-wrap" @click="saveImageToAlbum(state.posterFilePath)">
<view class="icon-wrap">
<uv-icon name="photo" size="30" color="#1B493E"></uv-icon>
</view>
<view class="text">{{ $t("referrerInfo.loadPoster") }}</view>
</view> -->
<view class="btn-wrap">
<view class="icon-wrap">
<uv-icon name="weixin-fill" size="30" color="#1B493E"></uv-icon>
</view>
<button open-type="share" class="share-btn"></button>
<view class="text">{{ $t("referrerInfo.forwardInvitation") }}</view>
</view>
</view>
<canvas type="2d" id="qrCodeCanvas" class="qrCode-hide" style="width: 300px; height: 300px;"></canvas>
</view>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue';
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app';
import { navbarHeightAndStatusBarHeight } from '@/utils/common.js';
import navBar from '@/components/navBar.vue';
//
import { useMainStore } from '@/store/index.js';
const { themeInfo, storeState } = useMainStore();
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import { getClientCustomerApi } from '@/Apis/clientCustomer.js';
const clientCustomerApi = getClientCustomerApi();
const state = reactive({
qrCodeBase64: '',
qrCodeUrl: '',
posterFilePath: '',
qrCodeFilePath: '',
canvasWidth: 750,
canvasHeight: 800
});
const getQrCodeBase64 = () => {
return new Promise((resolve, reject) => {
clientCustomerApi.GetMediatorQrCode().then((res) => {
if (res.code == 200) {
state.qrCodeBase64 = res.data;
resolve(true);
} else {
resolve(false);
}
}).catch(reject);
});
}
const getPosterImage = async () => {
uni.showLoading({ title: t('toast.generating'), mask: true });
try {
const canvas = await drawCanvas();
state.posterFilePath = await canvasToTempFile(canvas);
} catch (error) {
console.error('生成失败:', error);
uni.showToast({ title: t('toast.generateFail'), icon: 'none' });
} finally {
uni.hideLoading();
}
};
// Canvas
const initCanvas = (canvasId) => {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
if (res && res[0]) {
const canvas = res[0].node;
canvas.width = res[0].width;
canvas.height = res[0].height;
resolve(canvas);
} else {
reject(new Error('Canvas初始化失败'));
}
});
});
};
const drawCanvas = () => {
return new Promise(async (resolve, reject) => {
try {
await nextTick();
const canvas = await initCanvas('posterCanvas');
const ctx = canvas.getContext('2d');
const dpr = uni.getWindowInfo().pixelRatio || 1;
const width = canvas.width;
const height = canvas.height;
const displayWidth = width * dpr;;
const displayHeight = height * dpr;
canvas.width = displayWidth;
canvas.height = displayHeight;
ctx.scale(dpr, dpr);
let x = 0;
let y = 0;
let text = '';
let textMetrics = 0;
let imgHeight = 0;
ctx.fillStyle = '#1B493E';
ctx.fillRect(0, 0, width, height);
ctx.font = 'bold 36px PingFang SC';
ctx.fillStyle = '#FFFFFF';
text = '青春公寓 · 学生专享';
textMetrics = ctx.measureText(text);
x = (width - textMetrics.width) / 2;
y = 70;
ctx.fillText(text, x, y);
ctx.font = '14px PingFang SC';
ctx.fillStyle = '#FFFFFF';
text = '智能门禁 | 近校选址 | 极速网络';
textMetrics = ctx.measureText(text);
x = (width - textMetrics.width) / 2;
y += 46;
ctx.fillText(text, x, y);
const drawImage = (ctx) => {
return new Promise((resolve) => {
const img = canvas.createImage();
img.src = 'https://elitesysapartment.oss-cn-hongkong.aliyuncs.com/2025/0814/posterRoomImg.jpg';
img.onload = () => {
x = 20;
y += 50;
imgHeight = height - y - 150;
ctx.drawImage(img, x, y, width - x * 2, imgHeight);
resolve(true);
};
});
}
const drawPriceCards = (ctx) => {
const cardData = [
{ title: '单人房', price: '$ 17,625/月起' },
{ title: '双人房', price: '$ 8,378.95/月起' },
{ title: '三人房', price: '$ 5375/月起' }
];
let startY = y;
let startX = x + (width - x * 2) / 2;
const cardWidth = (width - x * 2) / 2;
const cardGap = 20;
const cardHeight = (imgHeight - cardGap * 2) / 3;
cardData.forEach((item, index) => {
//
ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
ctx.fillRect(startX, startY, cardWidth, cardHeight, 10);
ctx.fill();
//
ctx.font = "bold 18px PingFang SC";
ctx.fillStyle = "#FFFFFF";
ctx.fillText(item.title, startX + 15, startY + 40);
//
ctx.font = "16px PingFang SC";
ctx.fillStyle = "#FFFFFF";
ctx.fillText(item.price, startX + 15, startY + 70);
startY += cardHeight + cardGap;
});
}
const drawBottom = (ctx) => {
return new Promise((resolve) => {
y = y + imgHeight + 40;
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, y, width, height);
ctx.font = "bold 18px PingFang SC";
ctx.fillStyle = "#1B493E";
ctx.fillText("ONE PACE", 40, height - 60);
ctx.fillText("一步居", 40, height - 30);
const imgWidth = 80;
const imgY = (height - y - imgWidth) / 2;
const imgX = (width - 40 - imgWidth);
const qrCodeImage = canvas.createImage();
qrCodeImage.src = state.qrCodeBase64;
qrCodeImage.onload = () => {
ctx.drawImage(qrCodeImage, imgX, y + imgY, imgWidth, imgWidth);
resolve(true);
};
});
}
await drawImage(ctx);
// drawPriceCards(ctx);
await drawBottom(ctx);
resolve(canvas);
} catch {
reject(false);
}
});
}
// Canvas
const canvasToTempFile = (canvas) => {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvas: canvas,
success: res => resolve(res.tempFilePath),
fail: (error => {
console.log(error);
reject(false);
})
});
});
};
//
const saveImageToAlbum = (base64Path) => {
// if (!filePath) return;
// uni.saveImageToPhotosAlbum({
// filePath,
// success: () => {
// uni.showToast({ title: 'Success', icon: 'none' });
// },
// fail: (err) => {
// console.log(err, '');
// }
// });
const fs = uni.getFileSystemManager();
const filePath = `${wx.env.USER_DATA_PATH}/temp.png`;
fs.writeFile({
filePath,
data: base64Path.replace(/^data:image\/\w+;base64,/, ''),
encoding: 'base64',
success() {
uni.saveImageToPhotosAlbum({
filePath,
success() {
uni.showToast({ title: '保存成功' });
}
});
}
});
};
const getQrCodeImage = async () => {
try {
await nextTick();
const canvas = await initCanvas('qrCodeCanvas');
const ctx = canvas.getContext('2d');
const dpr = uni.getWindowInfo().pixelRatio || 1;
const width = canvas.width;
const height = canvas.height;
const displayWidth = width * dpr;
const displayHeight = height * dpr;
canvas.width = displayWidth;
canvas.height = displayHeight;
ctx.scale(dpr, dpr);
const qrCodeImage = canvas.createImage();
qrCodeImage.src = state.qrCodeBase64;
qrCodeImage.onload = async () => {
ctx.drawImage(qrCodeImage, 0, 0, width, height);
state.qrCodeFilePath = await canvasToTempFile(canvas);
};
} catch (error) {
console.error('生成二维码失败:', error);
}
}
onShareAppMessage(() => {
return {
// imageUrl: 'https://elitesysapartment.oss-cn-hongkong.aliyuncs.com/miniProgram/2025/0520/banner1.jpg',
title: '使用该链接注册登录,绑定专属中介',
path: `/pages/index/index?mediatorId=${storeState.userInfo.id}`
};
});
onLoad(async () => {
// canvas
const systemInfo = uni.getWindowInfo();
state.canvasWidth = systemInfo.screenWidth;
state.canvasHeight = systemInfo.screenHeight - (navbarHeightAndStatusBarHeight()?.navbarHeight || 80) - 100;
await getQrCodeBase64();
// getPosterImage();
// getQrCodeImage();
});
</script>
<style lang="scss" scoped>
@import '@/static/style/theme.scss';
.invitation-wrap {
position: relative;
width: 100%;
height: 100vh;
background: #F5F5EF;
overflow: hidden;
.QrCode{
display: flex;
justify-content: center;
align-items: center;
.qrcodeImg{
width: 500rpx;
height: 500rpx;
}
}
.qrCode-hide {
position: fixed;
left: 7500rpx;
}
.bottom-wrap {
display: flex;
align-items: center;
justify-content: space-around;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 160rpx;
border-top: 1px solid #EEEEEE;
background: #FFFFFF;
// background: rgba(0, 0, 0, 0.3);
.btn-wrap {
position: relative;
display: flex;
align-items: center;
flex-direction: column;
.icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
}
.share-btn {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
}
.text {
color: #666666;
font-size: 28rpx;
margin-top: 10rpx;
}
}
}
}
</style>

291
pagesb/invoice/index.vue Normal file
View File

@ -0,0 +1,291 @@
<template>
<view class="invoice-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="wrapBox">
<view class="top">
<view class="arrow">
<image src="/static/setOrder/selectArrow.png" mode=""></image>
</view>
{{ $t("invoice.valid") }}
</view>
<view class="content">
<view class="orderBox">
<uv-checkbox-group v-model="state.checkList">
<!-- @click="goInvoiceApply(item)" -->
<view
class="orderLi"
v-for="(item, index) in state.list"
:key="index"
>
<view class="leftCheckBox">
<uv-checkbox activeColor="black" :name="item" shape="circle">
</uv-checkbox>
</view>
<view class="left">
<view>{{ $t("invoice.pay") }}: {{ item.paymentTime }}</view>
<view>{{ $t("invoice.site") }}: {{ item.siteName }}</view>
<view>{{ $t("invoice.type") }}: {{ item.unitTypeName }}</view>
<view
>{{ $t("invoice.rent") }}:
{{ item.termOfLease?.match(/\d{4}-\d{2}-\d{2}/g)[0] }} - {{
item.termOfLease?.match(/\d{4}-\d{2}-\d{2}/g)[1]
}}</view
>
</view>
<view class="right">
<view>{{ item.lockerName }}</view>
<view>{{ currency }}{{ item.money }}</view>
</view>
</view>
</uv-checkbox-group>
</view>
<button class="contact" @click="goInvoiceApplyforRecord">
<uv-icon
name="file"
custom-prefix="custom-icon"
size="16"
:color="themeInfo.theme === 'default' ? '#045459' : '#0F2232'"
></uv-icon>
<text style="margin-left: 8px">{{ $t("invoice.record") }}</text>
</button>
</view>
<view class="bottom">
<!-- <text
>已选 {{ state.checkList.length }} 个订单
{{ checkRowMoney }} </text
> -->
<text>{{ $t("invoice.order", { number: state.checkList.length, money: checkRowMoney }) }}</text>
<text>{{ $t("invoice.tip") }}</text>
</view>
<view class="goApply">
<view class="checlBox">
<uv-checkbox-group @change="selectAll">
<uv-checkbox
activeColor="black"
name="all"
shape="circle"
:label="$t('invoice.allSelect')"
>
</uv-checkbox>
</uv-checkbox-group>
</view>
<view class="next">
<button size="large" @click="goInvoiceApply">{{ $t("invoice.nextStep") }}</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
//
import { reactive, computed } from "vue";
import { useMainStore } from "@/store/index.js";
const { themeInfo } = useMainStore();
import navBar from "@/components/navBar.vue";
import { useInvoiceApi } from "@/Apis/invoice";
import { onShow } from "@dcloudio/uni-app";
import { useI18n } from "vue-i18n";
import { currency } from "@/config/index.js";
const { t } = useI18n();
const getApi = useInvoiceApi();
const state = reactive({
list: [],
checkList: [],
});
const selectAll = (e) => {
if (e.length) {
state.checkList = state.list;
} else {
state.checkList = [];
}
};
const checkRowMoney = computed(() => {
return state.checkList.reduce((pre, cur) => {
return pre + Number(cur.money);
}, 0).toFixed(2);
});
const goInvoiceApply = (row) => {
if(state.checkList.length == 0){
uni.showToast({
title: t("invoice.selectOrder"),
icon: "none",
});
return
}
if (checkRowMoney.value <= 0) {
uni.showToast({
title: t("invoice.validMoney"),
icon: "none",
});
return;
}
const ids = state.checkList.map((item) => item.paymentId);
uni.navigateTo({
url: `/pagesb/invoiceApply/index?paymentId=${ids}&num=${checkRowMoney.value}`,
});
//
state.checkList = [];
};
const getDataList = () => {
uni.showLoading();
getApi.GetCanInvoiceList().then((res) => {
uni.hideLoading();
if (res.code == 200) {
state.list = res.data
} else {
state.list = [];
}
});
};
const goInvoiceApplyforRecord = ()=>{
uni.navigateTo({
url: "/pagesb/invoiceApplyforRecord/index",
});
}
onShow(() => {
getDataList();
});
</script>
<style lang="scss" scoped>
@import '@/static/style/theme.scss';
.invoice-wrap {
height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(
to bottom,
var(--right-linear),
var(--left-linear)
);
.wrapBox {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
overflow: auto;
.top {
height: 100rpx;
line-height: 100rpx;
font-size: 26rpx;
font-weight: 600;
padding: 0 40rpx;
display: flex;
align-items: center;
.arrow {
width: 20rpx;
display: flex;
margin-right: 6rpx;
image {
width: 20rpx;
height: 12rpx;
}
}
}
.content {
flex: 1;
width: 100%;
padding: 20rpx;
padding-bottom: 200px;
overflow-y: auto;
background-color: #fff;
position: relative;
.contact {
position: fixed;
bottom: 300rpx;
right: 0;
height: 74rpx;
background-color: var(--main-color);
border-radius: 45rpx 0rpx 0rpx 45rpx;
border: 4rpx solid var(--stress-text);
box-shadow: 0rpx 4rpx 10rpx 0rpx rgba(0, 0, 0, 0.1);
padding: 0 20rpx 0 30rpx;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
color: var(--stress-text);
font-size: 28rpx;
font-weight: 700;
}
.orderBox {
padding: 20rpx;
.orderLi {
width: 100%;
margin-bottom: 30rpx;
border-radius: 18rpx;
padding: 20rpx;
font-size: 24rpx;
background-color: var(--main-color);
display: flex;
.leftCheckBox {
width: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.left {
flex: 1;
}
.right {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 150rpx;
:nth-child(1) {
border-radius: 99rpx;
padding: 0 20rpx;
height: 40rpx;
line-height: 40rpx;
background-color: black;
color: var(--main-color);
margin-bottom: 10rpx;
}
:nth-child(2) {
font-size: 30rpx;
}
}
}
}
}
.bottom {
height: 100rpx;
font-size: 24rpx;
padding: 0 40rpx;
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--stress-color);
text:first-of-type {
margin-right: 10px;
}
text:last-of-type {
margin-left: 10px;
}
}
.goApply {
height: 130rpx;
display: flex;
justify-content: space-between;
padding: 10upx 40upx;
align-items: center;
.next {
button {
background-color: black;
color: #fff;
font-size: 28rpx;
border-radius: 99rpx;
}
}
}
}
}
</style>

View File

@ -0,0 +1,582 @@
<template>
<view
class="container"
:class="[`${themeInfo.theme}-theme`, `${themeInfo.language}`]"
>
<navBar />
<view class="content">
<view class="info">
<view class="i-header">
<view class="tabbox">
<view
class="li"
@click="changeValidType(1)"
:class="{ active: state.vaildType == 1 }"
>
{{ $t("invoiceApply.electronicInvoice") }}
</view>
<!-- <view
class="li"
@click="changeValidType(2)"
:class="{ active: state.vaildType == 2 }"
>
{{ $t("invoiceApply.paperInvoice") }}
</view> -->
<view class="bottom-line" :class="{ right: state.vaildType == 2 }"></view>
</view>
</view>
<view class="infobox">
<view class="personal">
<view class="select">
<view class="label"> * 发票类型 INVOICE TYPE </view>
<view class="inputBox">
<view class="value">
<uv-radio-group size="12" v-model="state.formData.applicationType" @change="changeApplicationType" placement="row">
<uv-radio :name="2" label="全电普票"></uv-radio>
<uv-radio :name="1" label="全电专票"></uv-radio>
</uv-radio-group>
</view>
</view>
</view>
<view class="select">
<view class="label"> * 抬头类型 HEADER TYPE </view>
<view class="inputBox">
<view class="value">
<uv-radio-group size="12" v-model="state.formData.titleType" placement="row">
<uv-radio :disabled="state.formData.applicationType==1" :name="1" label="个人/事业单位"></uv-radio>
<uv-radio :name="2" label="公司/企业单位"></uv-radio>
</uv-radio-group>
</view>
</view>
</view>
<view class="select">
<view class="label"> * 发票抬头 INVOICE TITLE </view>
<view class="inputBox">
<view class="value">
<input placeholder="发票抬头" type="text" v-model="state.formData.title" />
</view>
<!-- #ifdef MP-WEIXIN -->
<view @click="chooseInvoiceTitle" style="display: flex;align-items: center;font-size: 26rpx;">
抬头簿
<uv-icon name="file-text-fill" color="#000" size="26"></uv-icon>
</view>
<!-- #endif -->
</view>
</view>
<view class="select">
<view class="label"> {{ state.formData.titleType == 2 ? '*' : '-' }} 纳税人识别号 IDENTIFICATION NUMBER </view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.identificationNo" />
</view>
</view>
</view>
<view class="select" @click="state.expand = !state.expand">
<view class="label"> - 更多信息 REMARKS </view>
<view class="inputBox">
<view class="value">
<view class="label">{{ state.expand?'点击收起信息':'展开可填写购买方信息、备注等。' }} </view>
</view>
<view class="arrow" :class="{ arrowClose: !state.expand }" >
<image src="/static/setOrder/selectArrow.png" mode=""></image>
</view>
</view>
</view>
<template v-if="state.expand">
<!-- <view class="select">
<view class="label"> - 银行账号 ACCOUNT NUMBER</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.bank" />
</view>
</view>
</view> -->
<view class="select">
<view class="label"> - 银行账号 ACCOUNT NUMBER</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.bankAccount" />
</view>
</view>
</view>
<view class="select">
<view class="label"> - 开户银行 ACCOUNT OPENING</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.bank" />
</view>
</view>
</view>
<view class="select">
<view class="label"> - 公司地址 ADDRESS</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.address" />
</view>
</view>
</view>
<view class="select">
<view class="label"> - 公司电话 PHONE NUMBER</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.telephone" />
</view>
</view>
</view>
</template>
<view class="receiving" style="padding: 10rpx 0;">
* 收票邮箱 RECEIVING INFORMATION
</view>
<view class="select">
<view class="label"> * 收票邮箱 EMAIL</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.email" />
</view>
</view>
</view>
<!-- <view class="select">
<view class="label"> * 收票人姓名 NAME</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.recipient" />
</view>
</view>
</view>
<view class="select">
<view class="label"> * 收票人电话 PHONE</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.recipientPhone" />
</view>
</view>
</view>
<view class="select">
<view class="label"> * 收票人地址 ADDRESS</view>
<view class="inputBox">
<view class="value">
<input type="text" v-model="state.formData.recipientAddress" />
</view>
</view>
</view>-->
<view class="select">
<view class="label"> - 备注信息 REMARKS</view>
<view class="inputBox">
<view class="value">
<input type="text" placeholder="请填写需在发票备注栏中显示的内容" v-model="state.formData.remarks" />
</view>
</view>
</view>
<view class="select">
<view class="label"> - 发票金额 AMOUNT</view>
<view class="inputBox">
<view class="value">
{{ currency }} {{ state.formData.amount }}
</view>
</view>
</view>
<view class="btn">
<button class="next-btn" @click="submit()">
{{ $t("common.submit") }}
</button>
</view>
<view class="tips" style="padding: 10rpx 0;">
根据相关政策规定消费环节汇总各种形式的券积分等金额不支持开票
</view>
</view>
</view>
</view>
</view>
<!-- 是否验证成功 -->
<myModal v-model="state.showAuthModal" :content="state.contentTips" @confirm="confirm" :cancelShow="false"></myModal>
</view>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { navigateBack } from '@/utils/common.js';
import navBar from "@/components/navBar.vue";
//
import { useMainStore } from "@/store/index.js";
import myModal from "@/components/myModal.vue";
const { themeInfo, storeState } = useMainStore();
import { useI18n } from 'vue-i18n';
import { useLoginApi } from "@/Apis/login.js";
import { useInvoiceApi } from "@/Apis/invoice.js";
import { currency } from "@/config/index.js";
const { t } = useI18n();
const pages = getCurrentPages()
const getLoginApi = useLoginApi();
const getInvoiceApi = useInvoiceApi();
const canGetCode = ref();
const state = reactive({
expand: false,
tips: "获取验证码",
hasVeriPerson: false, //
hasVeriEnterprise: false, //
editPerson: true, //
editEnterprise: true, //
vaildType: 1, // 1 2
showAuthModal: false,
contentTips:'发票申请成功,请耐心等待工作人员审核。', //
formData: {
id:'',
paymentId:'',
applicationType: 2,// 1 2
titleType: 1,// 1 2
title: '',//
identificationNo:'',//
email: '', //
bankAccount: '', //
bank: '', //
address: '', //
telephone: '', //
remarks: '', //
amount: 0,
recipient: "",
recipientPhone: "",
recipientAddress: ""
},
});
const changeValidType = (val) => {
state.vaildType = val;
};
const changeApplicationType = (val) => {
if(state.formData.applicationType == 1){
state.formData.titleType = 2
}
}
onLoad((e) => {
console.log(e.id);
// e.id
if(e.id){
state.formData.id = e.id;
getDeteil(e.id)
}
state.formData.paymentId = e.paymentId?.split(',');
state.formData.amount = e.num;
});
const getDeteil = () => {
getInvoiceApi.GetInvoiceApplyForById({id:state.formData.id}).then((res) => {
if (res.code == 200) {
state.formData = res.data
}
});
}
const chooseInvoiceTitle = (val) => {
uni.chooseInvoiceTitle({
success(res) {
state.formData.titleType = res.type == 0 ? 2 : 1;
state.formData.title = res.title;
state.formData.identificationNo = res.taxNumber;
state.formData.address = res.companyAddress;
state.formData.telephone = res.telephone;
state.formData.bank = res.bankName;
state.formData.bankAccount = res.bankAccount;
}
})
}
const getPhoneNumber = (e) => {
if (!state.editPerson) return;
// if (state.hasPhone) {
// state.formData.phone = storeState.userInfo.phone;
// } else {
// }
uni.showLoading();
if (e.code) {
getLoginApi.GetPhoneNumber({code:e.code}).then((res) => {
if (res.code == 200) {
uni.hideLoading();
state.formData.phone = res.data;
}
});
} else {
uni.hideLoading();
uni.showToast({
title: t('validation.getPhoneFail'),
icon: "none",
duration: 2000,
});
}
}
const confirm = () => {
navigateBack()
// uni.redirectTo({
// url: '/pagesb/invoiceApplyforRecord/index'
// });
}
const submit = () => {
// todo
verifyPerson();
}
//
const verifyPerson = () => {
const params = {
...state.formData,
}
delete params.amount;
if (!state.formData.title) {
return uni.showToast({
title: "请填写抬头",
icon: 'none'
});
}
if(state.formData.titleType == 2){
if (!state.formData.identificationNo) return uni.showToast({
title: "请填写纳税人识别号",
icon: 'none'
});
}
// if(state.formData.applicationType == 1){
// if(!state.formData.bankAccount||!state.formData.bank||!state.formData.address||!state.formData.telephone) {
// uni.showToast({
// title: ",",
// icon: 'none'
// })
// return
// }
// }
if (!state.formData.email) return uni.showToast({
title: "请填写邮箱",
icon: 'none'
});
uni.showLoading({mask:true});
if(state.formData.id){
getInvoiceApi.UpdateInvoiceApplyFor(params).then(res => {
uni.hideLoading();
if (res.code == 200) {
state.showAuthModal = true;
}
});
}else{
getInvoiceApi.InvoiceApplyFor(params).then(res => {
uni.showLoading({mask:true})
if (res.code == 200) {
state.showAuthModal = true;
}
});
}
}
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
.container {
margin: 0;
padding: 0 20upx;
width: 100%;
min-height: 100vh;
padding-bottom: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(
to bottom,
var(--right-linear),
var(--left-linear2)
);
background-attachment: fixed;
.content {
width: 100%;
.infobox {
width: 100%;
padding: 20upx 30upx;
background-color: #ffffff;
position: relative;
&::before {
content: "";
z-index: 9;
position: absolute;
bottom: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(var(--left-linear2) 0px, var(--left-linear2) 5px, transparent 5px, transparent);
background-size: 14px 14px;
}
}
.btn {
margin-top: 60rpx;
margin-bottom: 20rpx;
button {
font-size: 40rpx;
color: var(--text-color);
background: var(--active-color);
border-radius: 10rpx;
box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.13);
}
}
.receiving{
font-size: 32rpx;
}
.tips{
font-size: 32rpx;
}
.select {
border-bottom: 1px dashed #d8d8d857;
margin-bottom: 16rpx;
.label {
color: #bec2ce;
font-size: 26rpx;
}
.info {
color: #242e42;
font-size: 22rpx;
display: flex;
justify-content: space-between;
font-weight: bold;
padding: 0 18rpx;
opacity: 0.7;
&.Total {
margin-top: 10rpx;
color: #242e42;
opacity: 1;
}
}
.garyBox {
background-color: rgba(216, 216, 216, 0.3);
border-radius: 8rpx;
padding: 2rpx 10rpx;
font-size: 20rpx;
font-weight: bold;
color: rgb(36, 46, 66);
display: flex;
align-items: center;
justify-content: space-between;
margin: 10rpx 4rpx;
}
.inputBox {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx;
margin-left: 5px;
:deep(.uv-radio){
margin-right: 20rpx;
}
.ImgUpload {
display: flex;
margin: 10upx 0;
::v-deep .uv-upload {
margin-right: 40rpx;
border-radius: 18rpx;
border:2px solid #9797974F;
.uv-upload__wrap__preview{
margin: 0;
}
}
.upLoadText{
font-size: 28upx;
text-align: center;
padding: 10upx 0;
background: var(--active-color);
border-radius: 0 0 18upx 18upx;
margin: -1px;
}
}
.value {
flex: 1;
display: flex;
flex-wrap: wrap;
color: #242e42;
font-size: 24rpx;
font-weight: bold;
input {
width: 100%;
}
}
.arrowClose{
//
transform: rotate(180deg);
}
.arrow {
width: auto;
font-size: 24rpx;
.codeBtn {
font-size: 24rpx;
background-color: #d1cbcb2d;
}
}
image {
width: 20rpx;
height: 12rpx;
}
}
}
}
.i-header {
position: relative;
&::before {
content: "";
position: absolute;
top: -7px;
left: 0;
width: 100%;
height: 14px;
background: radial-gradient(var(--right-linear) 0px, var(--right-linear) 5px, transparent 5px, transparent);
background-size: 14px 14px;
z-index: 9;
}
padding: 30upx 0;
width: 100%;
background-color: #f7f7f7;
color: #242e42;
.tabbox {
position: relative;
padding: 20upx 10%;
display: flex;
justify-content: space-around;
.li {
width: 50%;
background-color: transparent;
font-size: 32upx;
outline: none;
text-align: center;
}
.li.active {
font-weight: bold;
}
.bottom-line {
position: absolute;
// left: calc(50% - 220rpx);
bottom: 0;
width: 160rpx;
height: 2.5px;
border-radius: 20px;
background: var(--main-color);
transition: left 0.5s ease;
&.right {
left: calc(50% + 60rpx);
}
}
}
}
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<view class="invoice-wrap" :class="[`${themeInfo.theme}-theme`]">
<navBar></navBar>
<view class="wrapBox">
<view class="content">
<view class="orderBox">
<!-- @click="goInvoiceApply(item)" -->
<view
class="orderLi"
v-for="(item, index) in state.list"
:key="index"
>
<view class="left">
<view>{{ $t("invoice.serial") }}: <text>{{ item.id }}</text> </view>
<view>{{ $t("invoice.time") }}: {{ item.applicationTime }}</view>
<view>{{ $t("invoice.status") }}: {{ auditOption[item.audit] }}</view>
<view>{{ $t("invoice.unit") }}: {{ item.lockerNames?.length ? item.lockerNames.join("、") : "无"}}</view>
<view v-if="item.audit == 2">
{{ $t("common.note") }}: {{ item.rejectRemarks}}</view>
</view>
<view class="right">
<!-- <view>{{ item.lockerNames }}</view> -->
<view>{{ currency }}{{ item.money }}</view>
</view>
<view class="btn" v-if="item.audit == 0 || item.audit == 2">
<button @click="editApply(item.id)">{{ $t("common.edit") }}</button>
<button @click="cancelApply(item.id)">{{ $t("common.cancel") }}</button>
</view>
</view>
<view v-if="state.list.length == 0" class="noData">{{ $t('common.noData') }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
//
import { reactive, computed } from "vue";
import { useMainStore } from "@/store/index.js";
import { currency } from "@/config/index.js";
import navBar from "@/components/navBar.vue";
import { useInvoiceApi } from "@/Apis/invoice";
import { onShow } from "@dcloudio/uni-app";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { themeInfo } = useMainStore();
const getApi = useInvoiceApi();
const state = reactive({
list: [],
});
const auditOption = computed(() => {
return {
0: t("invoice.status0"),
1: t("invoice.status1"),
2: t("invoice.status2"),
3: t("invoice.status3"),
4: t("invoice.status4"),
}
});
const editApply = (id)=>{
uni.navigateTo({
url: '/pagesb/invoiceApply/index?id='+id
})
}
const cancelApply = (id)=>{
uni.showModal({
title: t("common.tip"),
content: t("common.cancelApply"),
success: function (res) {
if (res.confirm) {
getApi.CancelInvoiceApplyFor({id}).then((res) => {
if(res.code == 200){
uni.showToast({
title: t("common.cancelSuccess"),
icon: 'success',
duration: 2000
});
getDataList();
}else{
uni.showToast({
title: t("common.cancelFail"),
icon: 'error',
duration: 2000
});
}
}
);
} else if (res.cancel) {
console.log('用户点击取消');
}
}
})
}
const getDataList = () => {
uni.showLoading();
getApi.GetInvoiceApplyFor().then((res) => {
uni.hideLoading();
if (res.code == 200) {
state.list = res.data;
} else {
state.list = [];
}
});
};
onShow(() => {
getDataList();
});
</script>
<style lang="scss" scoped>
@import '@/static/style/theme.scss';
.invoice-wrap {
height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(
to bottom,
var(--right-linear),
var(--left-linear)
);
.noData{
text-align: center;
}
.wrapBox {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
overflow: auto;
.top {
height: 100rpx;
line-height: 100rpx;
font-size: 26rpx;
font-weight: 600;
padding: 0 40rpx;
display: flex;
align-items: center;
.arrow {
width: 20rpx;
display: flex;
margin-right: 6rpx;
image {
width: 20rpx;
height: 12rpx;
}
}
}
.content {
flex: 1;
width: 100%;
padding: 20rpx;
overflow-y: auto;
background-color: #fff;
position: relative;
.contact {
position: absolute;
bottom: 60rpx;
right: 0;
height: 74rpx;
background-color: var(--main-color);
border-radius: 45rpx 0rpx 0rpx 45rpx;
border: 4rpx solid var(--stress-text);
box-shadow: 0rpx 4rpx 10rpx 0rpx rgba(0, 0, 0, 0.1);
padding: 0 20rpx 0 30rpx;
z-index: 9;
display: flex;
justify-content: center;
align-items: center;
color: var(--stress-text);
font-size: 28rpx;
font-weight: 700;
}
.orderBox {
padding: 20rpx;
.orderLi {
width: 100%;
flex-wrap: wrap;
margin-bottom: 30rpx;
border-radius: 18rpx;
padding: 20rpx;
font-size: 24rpx;
background-color: var(--main-color);
display: flex;
.leftCheckBox {
width: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.left {
flex: 1;
line-height: 1.5;
}
.btn{
width: 100%;
display: flex;
margin-top: 20rpx;
padding-top: 10rpx;
border-top:1px solid #292222;
button {
min-width: 124rpx;
font-size: 22rpx;
color: var(--btn-color1);
background-color: var(--text-color);
border: none;
height: 56rpx;
border-radius: 28rpx;
line-height: 56rpx;
text-align: center;
font-weight: 700;
letter-spacing: 0.39px;
padding: 0;
}
}
.right {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 150rpx;
// :nth-child(1) {
// border-radius: 99rpx;
// padding: 0 20rpx;
// height: 40rpx;
// line-height: 40rpx;
// background-color: black;
// color: var(--main-color);
// margin-bottom: 10rpx;
// }
:nth-child(1) {
font-size: 34rpx;
font-weight: bold;
}
}
}
}
}
.bottom {
height: 100rpx;
line-height: 100rpx;
font-size: 24rpx;
padding: 0 40rpx;
display: flex;
justify-content: space-between;
background-color: var(--stress-color);
}
.goApply {
height: 130rpx;
display: flex;
justify-content: space-between;
padding: 10upx 40upx;
align-items: center;
.next {
button {
background-color: black;
color: #fff;
font-size: 28rpx;
border-radius: 99rpx;
}
}
}
}
}
</style>

View File

@ -0,0 +1,189 @@
<template>
<view class="container" :class="[`${themeInfo.theme}-theme`]">
<navBar class="navBar"></navBar>
<view class="wrapbox">
<view class="box" v-for="(item,index) in state.dataList" :key="index">
<view class="imageBox">
<image class="topImage" :src="baseImageUrl + item.imgUrl" mode="widthFix"></image>
</view>
<view class="title">
<view class="title1">{{ item.title }}</view>
<view class="title2">{{ item.viceTitle }}</view>
</view>
<view class="content">
<my-uv-collapse :ref="(e)=>setRefs(e,index)" :border="false" :clickable="false">
<my-uv-collapse-item :clickable="false" value="详情" name="Docs guide">
<view class="htmlbox">
<rich-text :nodes="item.content"></rich-text>
</view>
</my-uv-collapse-item>
</my-uv-collapse>
</view>
</view>
<view>
<view class="NoDataTips" v-if="state.dataList.length === 0">
<text>抱歉暂无活动敬请关注</text>
<text class="en">SORRY,THERE ARE NO EVENTS,STAY TUNED PLEASE.</text>
</view>
</view>
</view>
<!-- 邀请详情 -->
</view>
</template>
<script setup>
import navBar from "@/components/navBar.vue";
// import { makePhoneCall } from "/utils/common.js";
import { ref,nextTick } from "vue";
import { useLoginApi } from "/Apis/home.js";
import { onLoad, onShareAppMessage,onShow,} from "@dcloudio/uni-app";
import myUvCollapse from "@/pagesb/components/my-uv-collapse/components/uv-collapse/uv-collapse.vue";
import myUvCollapseItem from "@/pagesb/components/my-uv-collapse/components/uv-collapse-item/uv-collapse-item.vue";
//
import { useMainStore } from "@/store/index.js";
import { baseImageUrl } from "@/config/index.js";
const { themeInfo } = useMainStore();
const getApi = useLoginApi();
const state = ref({
dataList: [],
});
const collapseRefs = ref([]);
const setRefs = (ref,index) => {
collapseRefs.value[index] = ref;
};
onLoad((params) => {
getData();
});
onShow(() => {
// $nextTick(() => {
// // init
// collapseRefs?.value.init();
// uni.hideLoading();
// })
});
const getData = async () => {
//
const params = {
platformType: 1, //
forPage: 4, //
contentType: 5, // 1 9 5
};
uni.showLoading();
getApi.GetPageContent(params).then((res) => {
uni.hideLoading();
if(res.code === 200){
state.value.dataList = res.data;
nextTick(() => {
collapseRefs.value.forEach((item) => {
item.init();
});
});
}
});
};
onShareAppMessage((res) => {
if (res.from === "button") {
}
return shareParam;
});
</script>
<style lang="scss" scoped>
@import "@/static/style/theme.scss";
uni-page-body {
height: 100%;
background: linear-gradient(0deg, rgb(1, 169, 188), rgb(10, 132, 184));
overflow: auto;
}
.navBar {
position: relative;
}
.NoDataTips{
width: 100%;
padding: 20rpx;
margin-bottom: 40rpx;
border-radius: 20rpx;
background-color: var(--right-linear);
font-size: 22rpx;
font-weight: bold;
display: flex;
flex-direction: column;
line-height: 30rpx;
}
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: start;
width: 100%;
overflow-x: hidden;
min-height: 100vh;
background: linear-gradient(0deg, var(--left-linear), var(--right-linear));
.wrapbox {
width: 100%;
padding: 20rpx 20rpx;
min-height: 100vh;
background-color: #FFF;
.box{
background-color: var(--right-linear);
border-radius: 20rpx;
margin: 20rpx 0;
margin-bottom: 30px;
overflow: hidden;
.topImage{
width: 100%;
border-radius: 20rpx 20rpx 0 0;
}
.title{
padding: 20px;
padding-bottom: 0;
padding-top: 10px;
view{
display: flex;
align-items: center;
font-size: 22rpx;
font-weight: bold;
&::before{
display: block;
content: ' ';
width: 18rpx;
height: 18rpx;
background-color: black;
margin-right: 20rpx;
border-radius: 999px;
}
}
}
.content{
::v-deep(.uv-collapse){
.uv-cell__value{
font-size: 20rpx;
color: black;
font-weight: bold;
}
}
.htmlbox{
width: 100%;
background-color: rgb(250, 252, 243);
margin-right: 20rpx;
padding: 20rpx;
border-radius: 20rpx;
rich-text{
font-size: 22rpx;
}
}
}
}
}
}
.tips {
margin-top: 30rpx;
font-size: 20rpx;
font-weight: bold;
}
</style>

Some files were not shown because too many files have changed in this diff Show More