骑手端app代码仓库创建

This commit is contained in:
admin
2026-01-06 21:22:12 +08:00
commit 34c63780a8
467 changed files with 65334 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
<!-- TODO 是不是怎么复用 s-count-down 组件 -->
<template>
<view class="time" :style="justifyLeft">
<text class="" v-if="tipText">{{ tipText }}</text>
<text
class="styleAll p6"
v-if="isDay === true"
:style="{ background: bgColor.bgColor, color: bgColor.Color }"
>{{ day }}{{ bgColor.isDay ? '天' : '' }}</text
>
<text
class="timeTxt"
v-if="dayText"
:style="{ width: bgColor.timeTxtwidth, color: bgColor.bgColor }"
>{{ dayText }}</text
>
<text
class="styleAll"
:class="isCol ? 'timeCol' : ''"
:style="{ background: bgColor.bgColor, color: bgColor.Color, width: bgColor.width }"
>{{ hour }}</text
>
<text
class="timeTxt"
v-if="hourText"
:class="isCol ? 'whit' : ''"
:style="{ width: bgColor.timeTxtwidth, color: bgColor.bgColor }"
>{{ hourText }}</text
>
<text
class="styleAll"
:class="isCol ? 'timeCol' : ''"
:style="{ background: bgColor.bgColor, color: bgColor.Color, width: bgColor.width }"
>{{ minute }}</text
>
<text
class="timeTxt"
v-if="minuteText"
:class="isCol ? 'whit' : ''"
:style="{ width: bgColor.timeTxtwidth, color: bgColor.bgColor }"
>{{ minuteText }}</text
>
<text
class="styleAll"
:class="isCol ? 'timeCol' : ''"
:style="{ background: bgColor.bgColor, color: bgColor.Color, width: bgColor.width }"
>{{ second }}</text
>
<text class="timeTxt" v-if="secondText">{{ secondText }}</text>
</view>
</template>
<script>
export default {
name: 'countDown',
props: {
justifyLeft: {
type: String,
default: '',
},
//距离开始提示文字
tipText: {
type: String,
default: '倒计时',
},
dayText: {
type: String,
default: '天',
},
hourText: {
type: String,
default: '时',
},
minuteText: {
type: String,
default: '分',
},
secondText: {
type: String,
default: '秒',
},
datatime: {
type: Number,
default: 0,
},
isDay: {
type: Boolean,
default: true,
},
isCol: {
type: Boolean,
default: false,
},
bgColor: {
type: Object,
default: null,
},
},
data: function () {
return {
day: '00',
hour: '00',
minute: '00',
second: '00',
};
},
created: function () {
this.show_time();
},
mounted: function () {},
methods: {
show_time: function () {
let that = this;
function runTime() {
//时间函数
let intDiff = that.datatime - Date.parse(new Date()) / 1000; //获取数据中的时间戳的时间差;
let day = 0,
hour = 0,
minute = 0,
second = 0;
if (intDiff > 0) {
//转换时间
if (that.isDay === true) {
day = Math.floor(intDiff / (60 * 60 * 24));
} else {
day = 0;
}
hour = Math.floor(intDiff / (60 * 60)) - day * 24;
minute = Math.floor(intDiff / 60) - day * 24 * 60 - hour * 60;
second = Math.floor(intDiff) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
if (hour <= 9) hour = '0' + hour;
if (minute <= 9) minute = '0' + minute;
if (second <= 9) second = '0' + second;
that.day = day;
that.hour = hour;
that.minute = minute;
that.second = second;
} else {
that.day = '00';
that.hour = '00';
that.minute = '00';
that.second = '00';
}
}
runTime();
setInterval(runTime, 1000);
},
},
};
</script>
<style scoped>
.p6 {
padding: 0 8rpx;
}
.styleAll {
/* color: #fff; */
font-size: 24rpx;
height: 36rpx;
line-height: 36rpx;
border-radius: 6rpx;
text-align: center;
/* padding: 0 6rpx; */
}
.timeTxt {
text-align: center;
/* width: 16rpx; */
height: 36rpx;
line-height: 36rpx;
display: inline-block;
}
.whit {
color: #fff !important;
}
.time {
display: flex;
justify-content: center;
}
.red {
color: #fc4141;
margin: 0 4rpx;
}
.timeCol {
/* width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align:center;
border-radius: 6px;
background: #fff;
font-size: 24rpx; */
color: #e93323;
}
</style>

View File

@@ -0,0 +1,108 @@
<!-- 账号密码登录 accountLogin -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="ss-m-b-20">
<!-- <view class="head-title-active head-title-line" @tap="showAuthModal('smsLogin')">
短信登录
</view> -->
<view class="head-title head-title-animation">登录账号</view>
</view>
<view class="head-subtitle">如无账号请联系平台开通</view>
<!-- <view class="head-subtitle">如果未设置过密码请点击忘记密码</view> -->
</view>
<!-- 表单项 -->
<uni-forms
ref="accountLoginRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="账号">
<uni-easyinput placeholder="请输入手机号" v-model="state.model.mobile" :inputBorder="false">
<!-- <template v-slot:right>
<button class="ss-reset-button forgot-btn" @tap="showAuthModal('resetPassword')">
忘记密码
</button>
</template> -->
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="password" label="密码">
<uni-easyinput
type="password"
placeholder="请输入密码"
v-model="state.model.password"
:inputBorder="false"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="accountLoginSubmit">登录</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { mobile, password } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/member/auth';
const accountLoginRef = ref(null);
const emits = defineEmits(['onConfirm']);
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
// 数据
const state = reactive({
model: {
mobile: '', // 账号
password: '', // 密码
},
rules: {
mobile,
password,
},
});
// 账号登录
async function accountLoginSubmit() {
// 表单验证
const validate = await unref(accountLoginRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
// 同意协议
if (!props.agreeStatus) {
emits('onConfirm', true)
sheep.$helper.toast('请勾选同意');
return;
}
// 提交数据
const { code, data } = await AuthUtil.login(state.model);
if (code === 0) {
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,127 @@
<!-- 绑定/更换手机号 changeMobile -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20">
{{ userInfo.mobile ? '更换手机号' : '绑定手机号' }}
</view>
<view class="head-subtitle">为了您的账号安全请使用本人手机号码</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="changeMobileRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="state.model.mobile"
:inputBorder="false"
type="number"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn-start"
:disabled="state.isMobileEnd"
:class="{ 'code-btn-end': state.isMobileEnd }"
@tap="getSmsCode('changeMobile', state.model.mobile)"
>
{{ getSmsTimer('changeMobile') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
:inputBorder="false"
type="number"
maxlength="4"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="changeMobileSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<!-- 微信独有读取手机号 -->
<button
v-if="'WechatMiniProgram' === sheep.$platform.name"
class="ss-reset-button type-btn"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
使用微信手机号
</button>
</view>
</template>
<script setup>
import { computed, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { code, mobile } from '@/sheep/validate/form';
import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
const changeMobileRef = ref(null);
const userInfo = computed(() => sheep.$store('user').userInfo);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
// 绑定手机号
async function changeMobileSubmit() {
const validate = await unref(changeMobileRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 提交更新请求
const { code } = await UserApi.updateUserMobile(state.model);
if (code !== 0) {
return;
}
sheep.$store('user').getInfo();
closeAuthModal();
}
// 使用微信手机号
async function getPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
return;
}
const result = await sheep.$platform.useProvider().bindUserPhoneNumber(e.detail);
if (result) {
sheep.$store('user').getInfo();
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,113 @@
<!-- 修改密码登录时 -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20 head-title-animation text-bold">修改密码</view>
<view class="head-subtitle">您目前的登录密码为初始密码为了您的账号安全请对您的密码进行修改</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="changePasswordRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="reNewPassword" label="手机号">
<uni-easyinput
type="number"
placeholder="请输入手机号"
v-model="state.model.mobile"
:inputBorder="false"
maxlength="11"
>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="reNewPassword" label="新密码">
<uni-easyinput
type="password"
placeholder="请输入新密码"
v-model="state.model.newPassword"
:inputBorder="false"
maxlength="16"
>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="reNewPassword" label="确认密码">
<uni-easyinput
type="password"
placeholder="请重新输入新密码进行确认"
v-model="state.model.ackPassword"
:inputBorder="false"
maxlength="16"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="changePasswordSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<button class="ss-reset-button type-btn" @tap="closeAuthModal">
取消修改
</button>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import { ackPassword, newPassword, mobile } from '@/sheep/validate/form';
import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
const changePasswordRef = ref(null);
// 数据
const state = reactive({
model: {
mobile: '', // 手机号
newPassword: '', // 密码
ackPassword: '', // 密码
},
rules: {
mobile,
newPassword,
ackPassword
},
});
// 更改密码
async function changePasswordSubmit() {
// 参数校验
const validate = await unref(changePasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 发起请求
const { code } = await UserApi.updateUserPasswordReset(state.model);
if (code !== 0) {
return;
}
// 成功后,只需要关闭弹窗
closeAuthModal();
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
.text-bold {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,152 @@
<!-- 微信授权信息 mpAuthorization -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60 ss-flex-col">
<view class="ss-flex ss-m-b-20">
<view class="head-title ss-m-r-40 head-title-animation">授权信息</view>
</view>
<view class="head-subtitle">完善您的头像昵称手机号</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="accountLoginRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<!-- 获取头像昵称https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html -->
<uni-forms-item name="avatar" label="头像">
<button
class="ss-reset-button avatar-btn"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
<image
class="avatar-img"
:src="sheep.$url.cdn(state.model.avatar)"
mode="aspectFill"
@tap="sheep.$router.go('/pages/user/info')"
/>
<text class="cicon-forward" />
</button>
</uni-forms-item>
<uni-forms-item name="nickname" label="昵称">
<uni-easyinput
type="nickname"
placeholder="请输入昵称"
v-model="state.model.nickname"
:inputBorder="false"
/>
</uni-forms-item>
<view class="foot-box">
<button class="ss-reset-button authorization-btn" @tap="onConfirm"> 确认授权 </button>
</view>
</uni-forms>
</view>
</template>
<script setup>
import { computed, ref, reactive } from 'vue';
import sheep from '@/sheep';
import { closeAuthModal } from '@/sheep/hooks/useModal';
import FileApi from '@/sheep/api/infra/file';
import UserApi from '@/sheep/api/member/user';
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
const userInfo = computed(() => sheep.$store('user').userInfo);
const accountLoginRef = ref(null);
// 数据
const state = reactive({
model: {
nickname: userInfo.value.nickname,
avatar: userInfo.value.avatar,
},
rules: {},
disabledStyle: {
color: '#999',
disableColor: '#fff',
},
});
// 选择头像(来自微信)
function onChooseAvatar(e) {
const tempUrl = e.detail.avatarUrl || '';
uploadAvatar(tempUrl);
}
// 选择头像(来自文件系统)
async function uploadAvatar(tempUrl) {
if (!tempUrl) {
return;
}
let { data } = await FileApi.uploadFile(tempUrl);
state.model.avatar = data;
}
// 确认授权
async function onConfirm() {
const { model } = state;
const { nickname, avatar } = model;
if (!nickname) {
sheep.$helper.toast('请输入昵称');
return;
}
if (!avatar) {
sheep.$helper.toast('请选择头像');
return;
}
// 发起更新
const { code } = await UserApi.updateUser({
avatar: state.model.avatar,
nickname: state.model.nickname,
});
// 更新成功
if (code === 0) {
sheep.$helper.toast('授权成功');
await sheep.$store('user').getInfo();
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
.foot-box {
width: 100%;
display: flex;
justify-content: center;
}
.authorization-btn {
width: 686rpx;
height: 80rpx;
background-color: var(--ui-BG-Main);
border-radius: 40rpx;
color: #fff;
}
.avatar-img {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
}
.cicon-forward {
font-size: 30rpx;
color: #595959;
}
.avatar-btn {
width: 100%;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,119 @@
<!-- 重置密码未登录时 -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20">重置密码</view>
<view class="head-subtitle">为了您的账号安全设置密码前请先进行安全验证</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="resetPasswordRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="state.model.mobile"
type="number"
:inputBorder="false"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn code-btn-start"
:disabled="state.isMobileEnd"
:class="{ 'code-btn-end': state.isMobileEnd }"
@tap="getSmsCode('resetPassword', state.model.mobile)"
>
{{ getSmsTimer('resetPassword') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
type="number"
maxlength="4"
:inputBorder="false"
/>
</uni-forms-item>
<uni-forms-item name="password" label="密码">
<uni-easyinput
type="password"
placeholder="请输入密码"
v-model="state.model.password"
:inputBorder="false"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="resetPasswordSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<button v-if="!isLogin" class="ss-reset-button type-btn" @tap="showAuthModal('accountLogin')">
返回登录
</button>
</view>
</template>
<script setup>
import { computed, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { code, mobile, password } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
const resetPasswordRef = ref(null);
const isLogin = computed(() => sheep.$store('user').isLogin);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', // 手机号
code: '', // 验证码
password: '', // 密码
},
rules: {
code,
mobile,
password,
},
});
// 重置密码
const resetPasswordSubmit = async () => {
// 参数校验
const validate = await unref(resetPasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 发起请求
const { code } = await UserApi.resetUserPassword(state.model);
if (code !== 0) {
return;
}
// 成功后,用户重新登录
showAuthModal('accountLogin')
};
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,119 @@
<!-- 短信登录 - smsLogin -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="ss-flex ss-m-b-20">
<view class="head-title head-title-line head-title-animation">短信登录</view>
<view class="head-title-active ss-m-r-40" @tap="showAuthModal('accountLogin')">
账号登录
</view>
</view>
<view class="head-subtitle">未注册的手机号验证后自动注册账号</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="smsLoginRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="state.model.mobile"
:inputBorder="false"
type="number"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn code-btn-start"
:disabled="state.isMobileEnd"
:class="{ 'code-btn-end': state.isMobileEnd }"
@tap="getSmsCode('smsLogin', state.model.mobile)"
>
{{ getSmsTimer('smsLogin') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
:inputBorder="false"
type="number"
maxlength="4"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="smsLoginSubmit"> 登录 </button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { code, mobile } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/member/auth';
const smsLoginRef = ref(null);
const emits = defineEmits(['onConfirm']);
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
codeText: '获取验证码',
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
// 短信登录
async function smsLoginSubmit() {
// 参数校验
const validate = await unref(smsLoginRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
if (!props.agreeStatus) {
emits('onConfirm', true)
sheep.$helper.toast('请勾选同意');
return;
}
// 提交数据
const { code } = await AuthUtil.smsLogin(state.model);
if (code === 0) {
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,156 @@
@keyframes title-animation {
0% {
font-size: 32rpx;
}
100% {
font-size: 36rpx;
}
}
.login-wrap {
padding: 50rpx 34rpx;
min-height: 500rpx;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
}
.head-box {
.head-title {
min-width: 160rpx;
font-size: 36rpx;
// font-weight: bold;
color: #333333;
font-weight: 500;
line-height: 36rpx;
}
.head-title-active {
width: 160rpx;
font-size: 32rpx;
font-weight: 600;
color: #999;
line-height: 36rpx;
}
.head-title-animation {
text-align: center;
animation-name: title-animation;
animation-duration: 0.1s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
.head-title-line {
position: relative;
&::before {
content: '';
width: 1rpx;
height: 34rpx;
background-color: #e4e7ed;
position: absolute;
left: -30rpx;
top: 50%;
transform: translateY(-50%);
}
}
.head-subtitle {
// font-size: 26rpx;
font-weight: 400;
// color: #afb6c0;
font-size: 24rpx;
color: #999999;
// text-align: left;
text-align: center;
// display: flex;
}
}
// .code-btn[disabled] {
// background-color: #fff;
// }
.code-btn-start {
width: 160rpx;
height: 56rpx;
line-height: normal;
border: 2rpx solid var(--ui-BG-Main);
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 400;
color: var(--ui-BG-Main);
opacity: 1;
}
.forgot-btn {
width: 160rpx;
line-height: 56rpx;
font-size: 30rpx;
font-weight: 500;
color: #999;
}
.login-btn-start {
width: 158rpx;
height: 56rpx;
line-height: normal;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 500;
color: #fff;
}
.type-btn {
padding: 20rpx;
margin: 40rpx auto;
width: 200rpx;
font-size: 30rpx;
font-weight: 500;
color: #999999;
}
.auto-login-box {
width: 100%;
.auto-login-btn {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
margin: 0 30rpx;
}
.auto-login-img {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
}
}
.agreement-box {
margin: 80rpx auto 0;
.protocol-check {
transform: scale(0.7);
}
.agreement-text {
font-size: 26rpx;
font-weight: 500;
color: #999999;
.tcp-text {
color: var(--ui-BG-Main);
}
}
}
// 修改密码
.editPwd-btn-box {
.save-btn {
width: 690rpx;
line-height: 70rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
}
.forgot-btn {
width: 690rpx;
line-height: 70rpx;
font-size: 28rpx;
font-weight: 500;
color: #999999;
}
}

View File

@@ -0,0 +1,251 @@
<template>
<!-- 规格弹窗 -->
<su-popup :show="authType !== ''" round="10" :showClose="true" @close="closeAuthModal">
<view class="login-wrap main-blue">
<!-- 1. 账号密码登录 accountLogin -->
<account-login
v-if="authType === 'accountLogin'"
:agreeStatus="state.protocol"
@onConfirm="onConfirm"
/>
<!-- 2. 短信登录 smsLogin -->
<!-- <sms-login
v-if="authType === 'smsLogin'"
:agreeStatus="state.protocol"
@onConfirm="onConfirm"
/> -->
<!-- 3. 忘记密码 resetPassword-->
<reset-password v-if="authType === 'resetPassword'" />
<!-- 4. 绑定手机号 changeMobile -->
<change-mobile v-if="authType === 'changeMobile'" />
<!-- 5. 修改密码 changePassword-->
<changePassword v-if="authType === 'changePassword'" />
<!-- 6. 微信小程序授权 -->
<mp-authorization v-if="authType === 'mpAuthorization'" />
<!-- 7. 第三方登录 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="auto-login-box ss-flex ss-flex-col ss-row-center ss-col-center"
>
<!-- 7.1 微信小程序的快捷登录 -->
<view v-if="sheep.$platform.name === 'WechatMiniProgram'" class="ss-flex register-box">
<!-- <view class="register-title">还没有账号?</view> -->
<view class="register-title">已经拥有账号可以,</view>
<button
class="ss-reset-button login-btn"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
快捷登录
</button>
<view class="circle"></view>
</view>
<!-- 7.2 微信的公众号App小程序的登录基于 openid + code -->
<!-- <button
v-if="
['WechatOfficialAccount', 'WechatMiniProgram', 'App'].includes(sheep.$platform.name) &&
sheep.$platform.isWechatInstalled
"
@tap="thirdLogin('wechat')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
/>
</button> -->
<!-- 7.3 iOS 登录 TODO 芋艿:等后面搞 App 再弄 -->
<!-- <button
v-if="sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
@tap="thirdLogin('apple')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/apple.png')"
/>
</button> -->
</view>
<!-- 用户协议的勾选 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="agreement-box ss-flex ss-row-center"
:class="{ shake: currentProtocol }"
>
<label class="radio ss-flex ss-col-center" @tap="onChange">
<radio
:checked="state.protocol"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
@tap.stop="onChange"
/>
<view class="agreement-text ss-flex ss-col-center ss-m-l-8">
我已阅读并同意
<view class="tcp-text" @tap.stop="onProtocol('用户协议')"> 《用户协议》 </view>
<view class="agreement-text">与</view>
<view class="tcp-text" @tap.stop="onProtocol('隐私协议')"> 《隐私协议》 </view>
</view>
</label>
</view>
<view class="safe-box" />
</view>
</su-popup>
</template>
<script setup>
import { computed, reactive, ref } from 'vue';
import sheep from '@/sheep';
import accountLogin from './components/account-login.vue';
import smsLogin from './components/sms-login.vue';
import resetPassword from './components/reset-password.vue';
import changeMobile from './components/change-mobile.vue';
import changePassword from './components/change-password.vue';
import mpAuthorization from './components/mp-authorization.vue';
import { closeAuthModal, showAuthModal } from '@/sheep/hooks/useModal';
const modalStore = sheep.$store('modal');
// 授权弹窗类型
const authType = computed(() => modalStore.auth);
// const authType = "accountLogin";
const state = reactive({
protocol: false,
});
const currentProtocol = ref(false);
// 勾选协议
function onChange() {
state.protocol = !state.protocol;
}
// 查看协议
function onProtocol(title) {
closeAuthModal();
sheep.$router.go('/pages/public/richtext', {
title,
});
}
// 点击登录 / 注册事件
function onConfirm(e) {
currentProtocol.value = e;
setTimeout(() => {
currentProtocol.value = false;
}, 1000);
}
// 第三方授权登陆微信小程序、Apple
const thirdLogin = async (provider) => {
if (!state.protocol) {
currentProtocol.value = true;
setTimeout(() => {
currentProtocol.value = false;
}, 1000);
sheep.$helper.toast('请勾选同意');
return;
}
const loginRes = await sheep.$platform.useProvider(provider).login();
if (loginRes) {
const userInfo = await sheep.$store('user').getInfo();
closeAuthModal();
// 如果用户已经有头像和昵称,不需要再次授权
if (userInfo.avatar && userInfo.nickname) {
return;
}
// 触发小程序授权信息弹框
// #ifdef MP-WEIXIN
showAuthModal('mpAuthorization');
// #endif
}
};
// 微信小程序的“手机号快速验证”https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
const getPhoneNumber = async (e) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
sheep.$helper.toast('快捷登录失败');
return;
}
console.log("e", e);
let result = await sheep.$platform.useProvider().mobileLogin(e.detail);
console.log("result", result);
if (result) {
closeAuthModal();
}
};
</script>
<style lang="scss" scoped>
@import './index.scss';
.shake {
animation: shake 0.05s linear 4 alternate;
}
@keyframes shake {
from {
transform: translateX(-10rpx);
}
to {
transform: translateX(10rpx);
}
}
.register-box {
position: relative;
justify-content: center;
.register-btn {
color: #999999;
font-size: 30rpx;
font-weight: 500;
}
.register-title {
color: #999999;
font-size: 30rpx;
font-weight: 400;
margin-right: 24rpx;
}
.or-title {
margin: 0 16rpx;
color: #999999;
font-size: 30rpx;
font-weight: 400;
}
.login-btn {
color: var(--ui-BG-Main);
font-size: 30rpx;
font-weight: 500;
}
.circle {
position: absolute;
right: 0rpx;
top: 18rpx;
width: 8rpx;
height: 8rpx;
border-radius: 8rpx;
background: var(--ui-BG-Main);
}
}
.safe-box {
height: calc(constant(safe-area-inset-bottom) / 5 * 3);
height: calc(env(safe-area-inset-bottom) / 5 * 3);
}
.tcp-text {
color: var(--ui-BG-Main);
}
.agreement-text {
color: $dark-9;
}
</style>

View File

@@ -0,0 +1,69 @@
<!-- 顶部导航栏 - 单元格 -->
<template>
<view class="ss-flex ss-col-center">
<!-- 类型一 文字 -->
<view
v-if="data.type === 'text'"
class="nav-title inline"
:style="[{ color: data.textColor, width: width }]"
>
{{ data.text }}
</view>
<!-- 类型二 图片 -->
<view
v-if="data.type === 'image'"
:style="[{ width: width }]"
class="menu-icon-wrap ss-flex ss-row-center ss-col-center"
@tap="sheep.$router.go(data.url)"
>
<image class="nav-image radius-img" v-if="data.imgUrl == imgSrc" :src="userInfo.avatar?userInfo.avatar:defautAvatar" mode="aspectFit"></image>
<image class="nav-image" v-else :src="sheep.$url.cdn(data.imgUrl)" mode="aspectFit"></image>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { computed } from 'vue';
// 接收参数
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
width: {
type: String,
default: '1px',
},
});
const imgSrc = 'http://api.jnmall.zq-hightech.com/admin-api/infra/file/29/get/e2f8b02bae129322f99ed06226543a55a8c13226fa688017f1454508a974d7bb.png'
const defautAvatar = 'http://api.jnmall.zq-hightech.com/admin-api/infra/file/29/get/e2f8b02bae129322f99ed06226543a55a8c13226fa688017f1454508a974d7bb.png'
const userInfo = computed(() => sheep.$store('user').userInfo);
const height = computed(() => sheep.$platform.capsule.height);
</script>
<style lang="scss" scoped>
.nav-title {
font-size: 36rpx;
color: #333;
text-align: center;
}
.menu-icon-wrap {
.nav-image {
// height: 24px;
height: 65rpx;
width: 65rpx;
border-radius: 50% !important;
}
}
.radius-img {
}
</style>

View File

@@ -0,0 +1,314 @@
<template>
<su-fixed
:noFixed="props.noFixed"
:alway="props.alway"
:bgStyles="props.bgStyles"
:val="0"
:index="props.zIndex"
noNav
:bg="props.bg"
:ui="props.ui"
:opacity="props.opacity"
:placeholder="props.placeholder"
:sticky="props.sticky"
>
<su-status-bar />
<!--
:class="[{ 'border-bottom': !props.opacity && props.bg != 'bg-none' }]"
-->
<view class="ui-navbar-box">
<view
class="ui-bar"
:class="
props.status == '' ? `text-a` : props.status == 'light' ? 'text-white' : 'text-black'
"
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
>
<slot name="item"></slot>
<view class="right">
<!-- #ifdef MP -->
<view :style="[state.capsuleStyle]"></view>
<!-- #endif -->
</view>
</view>
</view>
</su-fixed>
</template>
<script setup>
/**
* 标题栏 - 基础组件navbar
*
* @param {Number} zIndex = 100 - 层级
* @param {Boolean} back = true - 是否返回上一页
* @param {String} backtext = '' - 返回文本
* @param {String} bg = 'bg-white' - 公共Class
* @param {String} status = '' - 状态栏颜色
* @param {Boolean} alway = true - 是否常驻
* @param {Boolean} opacity = false - 是否开启透明渐变
* @param {Boolean} opacityBg = false - 开启滑动渐变后,返回按钮是否添加背景
* @param {Boolean} noFixed = false - 是否浮动
* @param {String} ui = '' - 公共Class
* @param {Boolean} capsule = false - 是否开启胶囊返回
* @param {Boolean} stopBack = false - 是否禁用返回
* @param {Boolean} placeholder = true - 是否开启占位
* @param {Object} bgStyles = {} - 背景样式
*
*/
import { computed, reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
// 本地数据
const state = reactive({
statusCur: '',
capsuleStyle: {},
capsuleBack: {},
});
const sys_statusBar = sheep.$platform.device.statusBarHeight;
const sys_navBar = sheep.$platform.navbar;
const props = defineProps({
sticky: Boolean,
zIndex: {
type: Number,
default: 100,
},
back: {
//是否返回上一页
type: Boolean,
default: true,
},
backtext: {
//返回文本
type: String,
default: '',
},
bg: {
type: String,
default: 'bg-white',
},
status: {
//状态栏颜色 可以选择light dark/其他字符串视为黑色
type: String,
default: '',
},
// 常驻
alway: {
type: Boolean,
default: true,
},
opacity: {
//是否开启滑动渐变
type: Boolean,
default: false,
},
opacityBg: {
//开启滑动渐变后 返回按钮是否添加背景
type: Boolean,
default: false,
},
noFixed: {
//是否浮动
type: Boolean,
default: false,
},
ui: {
type: String,
default: '',
},
capsule: {
//是否开启胶囊返回
type: Boolean,
default: false,
},
stopBack: {
type: Boolean,
default: false,
},
placeholder: {
type: [Boolean],
default: true,
},
bgStyles: {
type: Object,
default() {},
},
});
const emits = defineEmits(['navback']);
onBeforeMount(() => {
init();
});
// 返回
const onNavback = () => {
sheep.$router.back();
};
// 初始化
const init = () => {
state.capsuleStyle = {
width: sheep.$platform.capsule.width + 'px',
height: sheep.$platform.capsule.height + 'px',
margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
};
state.capsuleBack = state.capsuleStyle;
};
</script>
<style lang="scss" scoped>
.ui-navbar-box {
background-color: transparent;
width: 100%;
.ui-bar {
position: relative;
z-index: 2;
white-space: nowrap;
display: flex;
position: relative;
align-items: center;
justify-content: space-between;
.left {
@include flex-bar;
.back {
@include flex-bar;
.back-icon {
@include flex-center;
width: 56rpx;
height: 56rpx;
margin: 0 10rpx;
font-size: 46rpx !important;
&.opacityIcon {
position: relative;
border-radius: 50%;
background-color: rgba(127, 127, 127, 0.5);
&::after {
content: '';
display: block;
position: absolute;
height: 200%;
width: 200%;
left: 0;
top: 0;
border-radius: inherit;
transform: scale(0.5);
transform-origin: 0 0;
opacity: 0.1;
border: 1px solid currentColor;
pointer-events: none;
}
&::before {
transform: scale(0.9);
}
}
}
/* #ifdef MP-ALIPAY */
._icon-back {
opacity: 0;
}
/* #endif */
}
.capsule {
@include flex-bar;
border-radius: 100px;
position: relative;
&.dark {
background-color: rgba(255, 255, 255, 0.5);
}
&.light {
background-color: rgba(0, 0, 0, 0.15);
}
&::after {
content: '';
display: block;
position: absolute;
height: 60%;
width: 1px;
left: 50%;
top: 20%;
background-color: currentColor;
opacity: 0.1;
pointer-events: none;
}
&::before {
content: '';
display: block;
position: absolute;
height: 200%;
width: 200%;
left: 0;
top: 0;
border-radius: inherit;
transform: scale(0.5);
transform-origin: 0 0;
opacity: 0.1;
border: 1px solid currentColor;
pointer-events: none;
}
.capsule-back,
.capsule-home {
@include flex-center;
flex: 1;
}
&.isFristPage {
.capsule-back,
&::after {
display: none;
}
}
}
}
.right {
@include flex-bar;
.right-content {
@include flex;
flex-direction: row-reverse;
}
}
.center {
@include flex-center;
text-overflow: ellipsis;
text-align: center;
flex: 1;
.image {
display: block;
height: 36px;
max-width: calc(100vw - 200px);
}
}
}
.ui-bar-bg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: 1;
pointer-events: none;
}
}
</style>

View File

@@ -0,0 +1,207 @@
<!-- 顶部导航栏 -->
<template>
<navbar
:alway="isAlways"
:back="false"
bg=""
:placeholder="isPlaceholder"
:bgStyles="bgStyles"
:opacity="isOpacity"
:sticky="sticky"
>
<template #item>
<view class="nav-box">
<view class="nav-icon" v-if="showLeftButton">
<view class="icon-box ss-flex" :class="{ 'inner-icon-box': data.styleType === 'inner' }">
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
<text class="sicon-back" v-if="hasHistory" />
<text class="sicon-home" v-else />
</view>
<view class="line"></view>
<view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
<text class="sicon-more" />
</view>
</view>
</view>
<view
class="nav-item"
v-for="(item, index) in navList"
:key="index"
:style="[parseImgStyle(item)]"
:class="[{ 'ss-flex ss-col-center ss-row-center': item.type !== 'search' }]"
>
<navbar-item :data="item" :width="parseImgStyle(item).width" />
</view>
</view>
</template>
</navbar>
</template>
<script setup>
/**
* 装修组件 - 自定义标题栏
*
*
* @property {Number | String} alwaysShow = [0,1] - 是否常驻
* @property {Number | String} styleType = [inner] - 是否沉浸式
* @property {String | Number} type - 标题背景模式
* @property {String} color - 页面背景色
* @property {String} src - 页面背景图片
*/
import { computed, unref } from 'vue';
import sheep from '@/sheep';
import Navbar from './components/navbar.vue';
import NavbarItem from './components/navbar-item.vue';
import { showMenuTools } from '@/sheep/hooks/useModal';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
showLeftButton: {
type: Boolean,
default: false,
},
});
const hasHistory = sheep.$router.hasHistory();
const sticky = computed(() => {
if (props.data.styleType === 'inner') {
if (props.data.alwaysShow) {
return false;
}
}
if (props.data.styleType === 'normal') {
return false;
}
});
const navList = computed(() => {
// #ifdef MP
return props.data.mpCells || [];
// #endif
return props.data.otherCells || [];
});
// 页面宽度
const windowWidth = sheep.$platform.device.windowWidth;
// 单元格宽度
const cell = computed(() => {
if (unref(navList).length) {
// 默认宽度为8个格子微信公众号右上角有胶囊按钮所以是6个格子
let cell = (windowWidth - 90) / 8;
// #ifdef MP
cell = (windowWidth - 80 - unref(sheep.$platform.capsule).width) / 6;
// #endif
return cell;
}
});
// 解析位置
const parseImgStyle = (item) => {
let obj = {
width: item.width * cell.value + (item.width - 1) * 10 + 'px',
left: item.left * cell.value + (item.left + 1) * 10 + 'px',
'border-radius': item.borderRadius + 'px',
};
return obj;
};
const isAlways = computed(() =>
props.data.styleType === 'inner' ? Boolean(props.data.alwaysShow) : true,
);
const isOpacity = computed(() =>
props.data.styleType === 'normal'
? false
: props.showLeftButton
? false
: props.data.styleType === 'inner',
);
const isPlaceholder = computed(() => props.data.styleType === 'normal');
const bgStyles = computed(() => {
return {
background:
props.data.bgType === 'img' && props.data.bgImg
? `url(${sheep.$url.cdn(props.data.bgImg)}) no-repeat top center / 100% 100%`
: props.data.bgColor,
};
});
// 左侧按钮:返回上一页或首页
function onClickLeft() {
if (hasHistory) {
sheep.$router.back();
} else {
sheep.$router.go('/pages/index/index');
}
}
// 右侧按钮:打开快捷菜单
function onClickRight() {
showMenuTools();
}
</script>
<style lang="scss" scoped>
.nav-box {
width: 750rpx;
position: relative;
height: 100%;
.nav-item {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.nav-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 20rpx;
.inner-icon-box {
border: 1px solid rgba(#fff, 0.4);
background: none !important;
}
.icon-box {
background: #ffffff;
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
border-radius: 30rpx;
width: 134rpx;
height: 56rpx;
margin-left: 8rpx;
.line {
width: 2rpx;
height: 24rpx;
background: #e5e5e7;
}
.sicon-back {
font-size: 32rpx;
}
.sicon-home {
font-size: 32rpx;
}
.sicon-more {
font-size: 32rpx;
}
.icon-button {
width: 67rpx;
height: 56rpx;
&-left:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 30rpx 0px 0px 30rpx;
}
&-right:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 0px 30rpx 30rpx 0px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<view
class="ss-flex-col ss-col-center ss-row-center empty-box"
:style="[{ paddingTop: paddingTop + 'rpx' }]"
>
<view class=""><image class="empty-icon" :src="icon" mode="widthFix"></image></view>
<view class="empty-text ss-m-t-28 ss-m-b-40">
<text v-if="text !== ''">{{ text }}</text>
</view>
<button class="ss-reset-button empty-btn" v-if="showAction" @tap="clickAction">
{{ actionText }}
</button>
</view>
</template>
<script setup>
import sheep from '@/sheep';
/**
* 容器组件 - 装修组件的样式容器
*/
const props = defineProps({
// 图标
icon: {
type: String,
default: '',
},
// 描述
text: {
type: String,
default: '',
},
// 是否显示button
showAction: {
type: Boolean,
default: false,
},
// button 文字
actionText: {
type: String,
default: '',
},
// 链接
actionUrl: {
type: String,
default: '',
},
// 间距
paddingTop: {
type: String,
default: '260',
},
//主题色
buttonColor: {
type: String,
default: 'var(--ui-BG-Main)',
},
});
const emits = defineEmits(['clickAction']);
function clickAction() {
if (props.actionUrl !== '') {
sheep.$router.go(props.actionUrl);
}
emits('clickAction');
}
</script>
<style lang="scss" scoped>
.empty-box {
width: 100%;
}
.empty-icon {
width: 240rpx;
}
.empty-text {
font-size: 26rpx;
font-weight: 500;
color: #999999;
}
.empty-btn {
width: 320rpx;
height: 70rpx;
border: 2rpx solid v-bind('buttonColor');
border-radius: 35rpx;
font-weight: 500;
color: v-bind('buttonColor');
font-size: 28rpx;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<view
class="page-app"
:class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
>
<view class="page-main" :style="[bgMain]">
<!-- 顶部导航栏-情况1默认通用顶部导航栏 -->
<su-navbar
v-if="navbar === 'normal'"
:title="title"
statusBar
:color="color"
:tools="tools"
:opacityBgUi="opacityBgUi"
@search="(e) => emits('search', e)"
:defaultSearch="defaultSearch"
/>
<!-- 顶部导航栏-情况2装修组件导航栏-标准 -->
<s-custom-navbar
v-else-if="navbar === 'custom' && navbarMode === 'normal'"
:data="navbarStyle"
:showLeftButton="showLeftButton"
/>
<view class="page-body" :style="[bgBody]">
<!-- 顶部导航栏-情况3沉浸式头部 -->
<su-inner-navbar v-if="navbar === 'inner'" :title="title" />
<view
v-if="navbar === 'inner'"
:style="[{ paddingTop: sheep.$platform.navbar + 'px' }]"
></view>
<!-- 顶部导航栏-情况4装修组件导航栏-沉浸式 -->
<s-custom-navbar
v-if="navbar === 'custom' && navbarMode === 'inner'"
:data="navbarStyle"
:showLeftButton="showLeftButton"
/>
<!-- 页面内容插槽 -->
<slot />
<!-- 底部导航 -->
<s-tabbar v-if="tabbar !== ''" :path="tabbar" />
</view>
</view>
<view class="page-modal">
<!-- 全局授权弹窗 -->
<s-auth-modal />
<!-- 全局分享弹窗 -->
<s-share-modal :shareInfo="shareInfo" />
<!-- 全局快捷入口 -->
<s-menu-tools />
</view>
</view>
</template>
<script setup>
/**
* 模板组件 - 提供页面公共组件,属性,方法
*/
import { computed, reactive, ref } from 'vue';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
import { onShow } from '@dcloudio/uni-app';
// #ifdef MP-WEIXIN
import { onShareAppMessage } from '@dcloudio/uni-app';
// #endif
const props = defineProps({
title: {
type: String,
default: '',
},
navbar: {
type: String,
default: 'normal',
},
opacityBgUi: {
type: String,
default: 'bg-white',
},
color: {
type: String,
default: '',
},
tools: {
type: String,
default: 'title',
},
keyword: {
type: String,
default: '',
},
navbarStyle: {
type: Object,
default: () => ({
styleType: '',
type: '',
color: '',
src: '',
list: [],
alwaysShow: 0,
}),
},
bgStyle: {
type: Object,
default: () => ({
src: '',
color: 'var(--ui-BG-1)',
}),
},
tabbar: {
type: [String, Boolean],
default: '',
},
onShareAppMessage: {
type: [Boolean, Object],
default: true,
},
leftWidth: {
type: [Number, String],
default: 100,
},
rightWidth: {
type: [Number, String],
default: 100,
},
defaultSearch: {
type: String,
default: '',
},
//展示返回按钮
showLeftButton: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['search']);
const sysStore = sheep.$store('sys');
const userStore = sheep.$store('user');
const appStore = sheep.$store('app');
const modalStore = sheep.$store('modal');
const sys = computed(() => sysStore);
// 导航栏模式(因为有自定义导航栏 需要计算)
const navbarMode = computed(() => {
if (props.navbar === 'normal' || props.navbarStyle.styleType === 'normal') {
return 'normal';
}
return 'inner';
});
// 背景1
const bgMain = computed(() => {
if (navbarMode.value === 'inner') {
return {
background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
props.bgStyle.backgroundImage,
)}) no-repeat top center / 100% auto`,
};
}
return {};
});
// 背景2
const bgBody = computed(() => {
if (navbarMode.value === 'normal') {
// return {
// background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
// props.bgStyle.backgroundImage,
// )}) no-repeat top center / 100% auto`,
// };
return {
background: `linear-gradient(to bottom, ${props.bgStyle.backgroundColor} 20%, #fafafa 50%)`,
// background: `linear-gradient( 180deg, #00B85B 0%, rgba(2,189,94,0.74) 66%, #07CC68 100%)`,
};
}
return {};
});
// 分享信息
const shareInfo = computed(() => {
if (props.onShareAppMessage === true) {
return sheep.$platform.share.getShareInfo();
} else {
if (!isEmpty(props.onShareAppMessage)) {
sheep.$platform.share.updateShareInfo(props.onShareAppMessage);
return props.onShareAppMessage;
}
}
return {};
});
// #ifdef MP-WEIXIN
// 微信小程序分享
onShareAppMessage(() => {
return {
title: shareInfo.value.title,
path: shareInfo.value.path,
imageUrl: shareInfo.value.image,
};
});
// #endif
onShow(() => {
if (!isEmpty(shareInfo.value)) {
sheep.$platform.share.updateShareInfo(shareInfo.value);
}
});
</script>
<style lang="scss" scoped>
.page-app {
position: relative;
color: var(--ui-TC);
background-color: var(--ui-BG-1) !important;
z-index: 2;
display: flex;
width: 100%;
height: 100vh;
.page-main {
position: absolute;
z-index: 1;
width: 100%;
min-height: 100%;
display: flex;
flex-direction: column;
.page-body {
width: 100%;
position: relative;
z-index: 1;
flex: 1;
}
.page-img {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
}
}
</style>

View File

@@ -0,0 +1,118 @@
<!-- 全局 - 快捷入口 -->
<template>
<su-popup :show="show" type="top" round="20" backgroundColor="#F0F0F0" @close="closeMenuTools">
<su-status-bar />
<view class="tools-wrap ss-m-x-30 ss-m-b-16">
<view class="title ss-m-b-34 ss-p-t-20">快捷菜单</view>
<view class="container-list ss-flex ss-flex-wrap">
<view class="list-item ss-m-b-24" v-for="item in list" :key="item.title">
<view class="ss-flex-col ss-col-center">
<button
class="ss-reset-button list-image ss-flex ss-row-center ss-col-center"
@tap="onClick(item)"
>
<image v-if="show" :src="sheep.$url.static(item.icon)" class="list-icon" />
</button>
<view class="list-title ss-m-t-20">{{ item.title }}</view>
</view>
</view>
</view>
</view>
</su-popup>
</template>
<script setup>
import { reactive, computed } from 'vue';
import sheep from '@/sheep';
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
const show = computed(() => sheep.$store('modal').menu);
function onClick(item) {
closeMenuTools();
if (item.url) sheep.$router.go(item.url);
}
const list = [
{
url: '/pages/index/index',
icon: '/static/img/shop/tools/home.png',
title: '首页',
},
{
url: '/pages/index/search',
icon: '/static/img/shop/tools/search.png',
title: '搜索',
},
{
url: '/pages/index/user',
icon: '/static/img/shop/tools/user.png',
title: '个人中心',
},
{
url: '/pages/index/cart',
icon: '/static/img/shop/tools/cart.png',
title: '购物车',
},
{
url: '/pages/user/goods-log',
icon: '/static/img/shop/tools/browse.png',
title: '浏览记录',
},
{
url: '/pages/user/goods-collect',
icon: '/static/img/shop/tools/collect.png',
title: '我的收藏',
},
{
url: '/pages/chat/index',
icon: '/static/img/shop/tools/service.png',
title: '客服',
},
];
</script>
<style lang="scss" scoped>
.tools-wrap {
// background: #F0F0F0;
// box-shadow: 0px 0px 28rpx 7rpx rgba(0, 0, 0, 0.13);
// opacity: 0.98;
// border-radius: 0 0 20rpx 20rpx;
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
}
.list-item {
width: calc(25vw - 20rpx);
.list-image {
width: 104rpx;
height: 104rpx;
border-radius: 52rpx;
background: var(--ui-BG);
.list-icon {
width: 54rpx;
height: 54rpx;
}
}
.list-title {
font-size: 26rpx;
font-weight: 500;
color: #333333;
}
}
}
.uni-popup {
top: 0 !important;
}
:deep(.button-hover) {
background: #fafafa !important;
}
</style>

View File

@@ -0,0 +1,168 @@
<!-- 海报弹窗 -->
<template>
<su-popup :show="show" round="10" @close="onClosePoster" type="center" class="popup-box">
<view class="ss-flex-col ss-col-center ss-row-center">
<image
v-if="!!painterImageUrl"
class="poster-img"
:src="painterImageUrl"
:style="{
height: poster.css.height+ 'px',
width: poster.css.width + 'px',
}"
:show-menu-by-longpress="true"
/>
</view>
<view
class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
v-if="!!painterImageUrl"
>
<button class="cancel-btn ss-reset-button" @tap="onClosePoster">取消</button>
<button class="save-btn ss-reset-button ui-BG-Main" @tap="onSavePoster">
{{
['wechatOfficialAccount', 'H5'].includes(sheep.$platform.name)
? '长按图片保存'
: '保存图片'
}}
</button>
</view>
<!-- 海报画板默认隐藏只用来生成海报生成方式为主动调用 -->
<l-painter
isCanvasToTempFilePath
pathType="url"
@success="setPainterImageUrl"
hidden
ref="painterRef"
/>
</su-popup>
</template>
<script setup>
/**
* 海报生成和展示
* 提示:小程序码默认跳转首页,由首页进行 spm 参数解析后跳转到对应的分享页面
* @description 用于生成分享海报,如:分享商品海报。
* @tutorial https://ext.dcloud.net.cn/plugin?id=2389
* @property {Boolean} show 弹出层控制
* @property {Object} shareInfo 分享信息
*/
import { reactive, ref, unref } from 'vue';
import sheep from '@/sheep';
import { getPosterData } from '@/sheep/components/s-share-modal/canvas-poster/poster';
const props = defineProps({
show: {
type: Boolean,
default: false,
},
shareInfo: {
type: Object,
default: () => {
},
},
});
const poster = reactive({
css: {
// 根节点若无尺寸,自动获取父级节点
width: sheep.$platform.device.windowWidth * 0.9,
height: 600,
},
views: [],
});
const emits = defineEmits(['success', 'close']);
const onClosePoster = () => {
emits('close');
};
const painterRef = ref(); // 海报画板
const painterImageUrl = ref(); // 海报 url
// 渲染海报
const renderPoster = async () => {
await painterRef.value.render(unref(poster));
};
// 获得生成的图片
const setPainterImageUrl = (path) => {
painterImageUrl.value = path;
};
// 保存海报图片
const onSavePoster = () => {
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
sheep.$helper.toast('请长按图片保存');
return;
}
// 非H5 保存到相册
uni.saveImageToPhotosAlbum({
filePath: painterImageUrl.value,
success: (res) => {
onClosePoster();
sheep.$helper.toast('保存成功');
},
fail: (err) => {
sheep.$helper.toast('保存失败');
console.log('图片保存失败:', err);
},
});
};
// 获得海报数据
async function getPoster() {
painterImageUrl.value = undefined
poster.views = await getPosterData({
width: poster.css.width,
shareInfo: props.shareInfo,
});
await renderPoster();
}
defineExpose({
getPoster,
});
</script>
<style lang="scss" scoped>
.popup-box {
position: relative;
}
.poster-title {
color: #999;
}
// 分享海报
.poster-btn-box {
width: 600rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -80rpx;
.cancel-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
background: $white;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
.save-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
}
}
.poster-img {
border-radius: 20rpx;
}
</style>

View File

@@ -0,0 +1,125 @@
import sheep from '@/sheep';
import { formatImageUrlProtocol, getWxaQrcode } from './index';
const goods = async (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
return [
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg)),
css: {
width,
position: 'fixed',
'object-fit': 'contain',
top: '0',
left: '0',
zIndex: -1,
},
},
{
type: 'text',
text: userInfo.nickname,
css: {
color: '#333',
fontSize: 16,
fontFamily: 'sans-serif',
position: 'fixed',
top: width * 0.06,
left: width * 0.22,
},
},
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
css: {
position: 'fixed',
left: width * 0.04,
top: width * 0.04,
width: width * 0.14,
height: width * 0.14,
},
},
{
type: 'image',
src: formatImageUrlProtocol(poster.shareInfo.poster.image),
css: {
position: 'fixed',
left: width * 0.03,
top: width * 0.21,
width: width * 0.94,
height: width * 0.94,
},
},
{
type: 'text',
text: poster.shareInfo.poster.title,
css: {
position: 'fixed',
left: width * 0.04,
top: width * 1.18,
color: '#333',
fontSize: 14,
lineHeight: 15,
maxWidth: width * 0.91,
},
},
{
type: 'text',
text: '¥' + poster.shareInfo.poster.price,
css: {
position: 'fixed',
left: width * 0.04,
top: width * 1.31,
fontSize: 20,
fontFamily: 'OPPOSANS',
color: '#333',
},
},
{
type: 'text',
text:
poster.shareInfo.poster.original_price > 0
? '¥' + poster.shareInfo.poster.original_price
: '',
css: {
position: 'fixed',
left: width * 0.3,
top: width * 1.33,
color: '#999',
fontSize: 10,
fontFamily: 'OPPOSANS',
textDecoration: 'line-through',
},
},
// #ifndef MP-WEIXIN
{
type: 'qrcode',
text: poster.shareInfo.link,
css: {
position: 'fixed',
left: width * 0.75,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
// #ifdef MP-WEIXIN
{
type: 'image',
src: wxa_qrcode,
css: {
position: 'fixed',
left: width * 0.75,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
];
};
export default goods;

View File

@@ -0,0 +1,122 @@
import sheep from '@/sheep';
import { formatImageUrlProtocol, getWxaQrcode } from './index';
const groupon = async (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
return [
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.groupon_bg)),
css: {
width,
position: 'fixed',
'object-fit': 'contain',
top: '0',
left: '0',
zIndex: -1,
},
},
{
type: 'text',
text: userInfo.nickname,
css: {
color: '#333',
fontSize: 16,
fontFamily: 'sans-serif',
position: 'fixed',
top: width * 0.06,
left: width * 0.22,
},
},
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
css: {
position: 'fixed',
left: width * 0.04,
top: width * 0.04,
width: width * 0.14,
height: width * 0.14,
},
},
{
type: 'image',
src: formatImageUrlProtocol(poster.shareInfo.poster.image),
css: {
position: 'fixed',
left: width * 0.03,
top: width * 0.21,
width: width * 0.94,
height: width * 0.94,
borderRadius: 10,
},
},
{
type: 'text',
text: poster.shareInfo.poster.title,
css: {
color: '#333',
fontSize: 14,
position: 'fixed',
top: width * 1.18,
left: width * 0.04,
maxWidth: width * 0.91,
lineHeight: 5,
},
},
{
type: 'text',
text: '¥' + poster.shareInfo.poster.price,
css: {
color: '#ff0000',
fontSize: 20,
fontFamily: 'OPPOSANS',
position: 'fixed',
top: width * 1.3,
left: width * 0.04,
},
},
{
type: 'text',
text: '2人团',
css: {
color: '#fff',
fontSize: 12,
fontFamily: 'OPPOSANS',
position: 'fixed',
left: width * 0.84,
top: width * 1.3,
},
},
// #ifndef MP-WEIXIN
{
type: 'qrcode',
text: poster.shareInfo.link,
css: {
position: 'fixed',
left: width * 0.5,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
// #ifdef MP-WEIXIN
{
type: 'image',
src: wxa_qrcode,
css: {
position: 'fixed',
left: width * 0.75,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
];
};
export default groupon;

View File

@@ -0,0 +1,39 @@
import user from './user';
import goods from './goods';
import groupon from './groupon';
import SocialApi from '@/sheep/api/member/social';
export function getPosterData(options) {
switch (options.shareInfo.poster.type) {
case 'user':
return user(options);
case 'goods':
return goods(options);
case 'groupon':
return groupon(options);
}
}
export function formatImageUrlProtocol(url) {
// #ifdef H5
// H5平台 https协议下需要转换
if (window.location.protocol === 'https:' && url.indexOf('http:') === 0) {
url = url.replace('http:', 'https:');
}
// #endif
// #ifdef MP-WEIXIN
// 小程序平台 需要强制转换为https协议
if (url.indexOf('http:') === 0) {
url = url.replace('http:', 'https:');
}
// #endif
return url;
}
// 获得微信小程序码 Base64 image
export async function getWxaQrcode(path, query) {
const res = await SocialApi.getWxaQrcode(path, query);
return 'data:image/png;base64,' + res.data;
}

View File

@@ -0,0 +1,74 @@
import sheep from '@/sheep';
import { formatImageUrlProtocol, getWxaQrcode } from './index';
const user = async (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
return [
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.user_bg)),
css: {
width,
position: 'fixed',
'object-fit': 'contain',
top: '0',
left: '0',
zIndex: -1,
},
},
{
type: 'text',
text: userInfo.nickname,
css: {
color: '#333',
fontSize: 14,
textAlign: 'center',
fontFamily: 'sans-serif',
position: 'fixed',
top: width * 0.4,
left: width / 2,
},
},
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
css: {
position: 'fixed',
left: width * 0.4,
top: width * 0.16,
width: width * 0.2,
height: width * 0.2,
},
},
// #ifndef MP-WEIXIN
{
type: 'qrcode',
text: poster.shareInfo.link,
css: {
position: 'fixed',
left: width * 0.35,
top: width * 0.84,
width: width * 0.3,
height: width * 0.3,
},
},
// #endif
// #ifdef MP-WEIXIN
{
type: 'image',
src: wxa_qrcode,
css: {
position: 'fixed',
left: width * 0.35,
top: width * 0.84,
width: width * 0.3,
height: width * 0.3,
},
},
// #endif
];
};
export default user;

View File

@@ -0,0 +1,196 @@
<!-- 全局分享弹框 -->
<template>
<view>
<su-popup :show="state.showShareGuide" :showClose="false" @close="onCloseGuide" />
<view v-if="state.showShareGuide" class="guide-wrap">
<image class="guide-image" :src="sheep.$url.static('/static/img/shop/share/share_guide.png')" />
</view>
<su-popup :show="show" round="10" :showClose="false" @close="closeShareModal">
<!-- 分享 tools -->
<view class="share-box">
<view class="share-list-box ss-flex">
<!-- 操作 发送给微信好友 -->
<button
v-if="shareConfig.methods.includes('forward')"
class="share-item share-btn ss-flex-col ss-col-center"
open-type="share"
@tap="onShareByForward"
>
<image class="share-img" :src="sheep.$url.static('/static/img/shop/share/share_wx.png')" mode="" />
<text class="share-title">微信好友</text>
</button>
<!-- 操作 生成海报图片 -->
<button
v-if="shareConfig.methods.includes('poster')"
class="share-item share-btn ss-flex-col ss-col-center"
@tap="onShareByPoster"
>
<image
class="share-img"
:src="sheep.$url.static('/static/img/shop/share/share_poster.png')"
mode=""
/>
<text class="share-title">生成海报</text>
</button>
<!-- 操作 生成链接 -->
<button
v-if="shareConfig.methods.includes('link')"
class="share-item share-btn ss-flex-col ss-col-center"
@tap="onShareByCopyLink"
>
<image class="share-img" :src="sheep.$url.static('/static/img/shop/share/share_link.png')" mode="" />
<text class="share-title">复制链接</text>
</button>
</view>
<view class="share-foot ss-flex ss-row-center ss-col-center" @tap="closeShareModal">
取消
</view>
</view>
</su-popup>
<!-- 分享海报对应操作 -->
<canvas-poster
ref="SharePosterRef"
:show="state.showPosterModal"
:shareInfo="shareInfo"
@close="state.showPosterModal = false"
/>
</view>
</template>
<script setup>
/**
* 分享弹窗
*/
import { ref, unref, reactive, computed } from 'vue';
import sheep from '@/sheep';
import canvasPoster from './canvas-poster/index.vue';
import { closeShareModal, showAuthModal } from '@/sheep/hooks/useModal';
const show = computed(() => sheep.$store('modal').share);
const shareConfig = computed(() => sheep.$store('app').platform.share);
const SharePosterRef = ref('');
const props = defineProps({
shareInfo: {
type: Object,
default() {},
},
});
const state = reactive({
showShareGuide: false, // H5 的指引
showPosterModal: false, // 海报弹窗
});
// 操作 ②:生成海报分享
const onShareByPoster = () => {
closeShareModal();
if (!sheep.$store('user').isLogin) {
showAuthModal();
return;
}
console.log(props.shareInfo);
unref(SharePosterRef).getPoster();
state.showPosterModal = true;
};
// 操作 ①:直接转发分享
const onShareByForward = () => {
closeShareModal();
// #ifdef H5
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
state.showShareGuide = true;
return;
}
// #endif
// #ifdef APP-PLUS
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 0,
href: props.shareInfo.link,
title: props.shareInfo.title,
summary: props.shareInfo.desc,
imageUrl: props.shareInfo.image,
success: (res) => {
console.log('success:' + JSON.stringify(res));
},
fail: (err) => {
console.log('fail:' + JSON.stringify(err));
},
});
// #endif
};
// 操作 ③:复制链接分享
const onShareByCopyLink = () => {
sheep.$helper.copyText(props.shareInfo.link);
closeShareModal();
};
function onCloseGuide() {
state.showShareGuide = false;
}
</script>
<style lang="scss" scoped>
.guide-image {
right: 30rpx;
top: 0;
position: fixed;
width: 580rpx;
height: 430rpx;
z-index: 10080;
}
// 分享tool
.share-box {
background: $white;
width: 750rpx;
border-radius: 30rpx 30rpx 0 0;
padding-top: 30rpx;
.share-foot {
font-size: 24rpx;
color: $gray-b;
height: 80rpx;
border-top: 1rpx solid $gray-e;
}
.share-list-box {
.share-btn {
background: none;
border: none;
line-height: 1;
padding: 0;
&::after {
border: none;
}
}
.share-item {
flex: 1;
padding-bottom: 20rpx;
.share-img {
width: 70rpx;
height: 70rpx;
background: $gray-f;
border-radius: 50%;
margin-bottom: 20rpx;
}
.share-title {
font-size: 24rpx;
color: $dark-6;
}
}
}
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<view class="u-page__item" v-if="tabbar?.items?.length > 0">
<su-tabbar
:value="path"
:fixed="true"
:placeholder="true"
:safeAreaInsetBottom="true"
:inactiveColor="tabbar.style.color"
:activeColor="tabbar.style.activeColor"
:midTabBar="tabbar.mode === 2"
:customStyle="tabbarStyle"
>
<su-tabbar-item
v-for="(item, index) in tabbar.items"
:key="item.text"
:text="item.text"
:name="item.url"
:isCenter="getTabbarCenter(index)"
:centerImage="sheep.$url.cdn(item.iconUrl)"
@tap="sheep.$router.go(item.url)"
>
<template v-slot:active-icon>
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.activeIconUrl)"></image>
</template>
<template v-slot:inactive-icon>
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.iconUrl)"></image>
</template>
</su-tabbar-item>
</su-tabbar>
</view>
</template>
<script setup>
import { computed, unref } from 'vue';
import sheep from '@/sheep';
const tabbar = computed(() => {
return sheep.$store('app').template.basic?.tabbar;
});
const tabbarStyle = computed(() => {
const backgroundStyle = tabbar.value.style;
if (backgroundStyle.bgType === 'color') {
return { background: backgroundStyle.bgColor };
}
if (backgroundStyle.bgType === 'img')
return {
background: `url(${sheep.$url.cdn(
backgroundStyle.bgImg,
)}) no-repeat top center / 100% auto`,
};
});
const getTabbarCenter = (index) => {
if (unref(tabbar).mode !== 2) return false;
return unref(tabbar).items % 2 > 0
? Math.ceil(unref(tabbar).items.length / 2) === index + 1
: false;
};
const props = defineProps({
path: String,
default: '',
});
</script>
<style lang="scss">
.u-page {
padding: 0;
&__item {
&__title {
color: var(--textSize);
background-color: #fff;
padding: 15px;
font-size: 15px;
&__slot-title {
color: var(--textSize);
font-size: 14px;
}
}
&__slot-icon {
width: 25px;
height: 25px;
}
}
}
</style>