初始化移动端提交

This commit is contained in:
chenbowen
2025-09-30 00:08:23 +08:00
parent 08784ca8f3
commit f2ffc65094
406 changed files with 55626 additions and 93 deletions

50
sheep/api/democontract.js Normal file
View File

@@ -0,0 +1,50 @@
import request from '@/sheep/request';
const DemoContractApi = {
// 查询示例合同分页
getDemoContractPage: (params) => {
return request({
url: '/demo-contract/page',
method: 'GET',
params,
});
},
// 查询示例合同详情
getDemoContract: (id) => {
return request({
url: '/demo-contract/get',
method: 'GET',
params: { id },
});
},
// 新增示例合同
createDemoContract: (data) => {
return request({
url: '/demo-contract/create',
method: 'POST',
data,
});
},
// 修改示例合同
updateDemoContract: (data) => {
return request({
url: '/demo-contract/update',
method: 'PUT',
data,
});
},
// 删除示例合同
deleteDemoContract: (id) => {
return request({
url: '/demo-contract/delete',
method: 'DELETE',
params: { id },
});
},
};
export default DemoContractApi;

11
sheep/api/index.js Normal file
View File

@@ -0,0 +1,11 @@
// 目的解决微信小程序的「代码质量」在「JS 文件」提示:主包内,不应该存在主包未使用的 JS 文件
const files = import.meta.glob('./*/*.js', { eager: true });
let api = {};
Object.keys(files).forEach((key) => {
api = {
...api,
[key.replace(/(.*\/)*([^.]+).*/gi, '$2')]: files[key].default,
};
});
export default api;

View File

@@ -0,0 +1,50 @@
import request from '@/sheep/request';
const DemoContractApi = {
// 查询示例合同分页
getDemoContractPage: (params) => {
return request({
url: '/template/demo-contract/page',
method: 'GET',
params,
});
},
// 查询示例合同详情
getDemoContract: (id) => {
return request({
url: '/template/demo-contract/get',
method: 'GET',
params: { id },
});
},
// 新增示例合同
createDemoContract: (data) => {
return request({
url: '/template/demo-contract/create',
method: 'POST',
data,
});
},
// 修改示例合同
updateDemoContract: (data) => {
return request({
url: '/template/demo-contract/update',
method: 'PUT',
data,
});
},
// 删除示例合同
deleteDemoContract: (id) => {
return request({
url: '/template/demo-contract/delete',
method: 'DELETE',
params: { id },
});
},
};
export default DemoContractApi;

67
sheep/api/infra/file.js Normal file
View File

@@ -0,0 +1,67 @@
import { baseUrl, apiPath, tenantId } from '@/sheep/config';
import request, { getAccessToken } from '@/sheep/request';
const FileApi = {
// 上传文件
uploadFile: (file, directory = '') => {
uni.showLoading({
title: '上传中',
});
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + apiPath + '/infra/file/upload',
filePath: file,
name: 'file',
header: {
Accept: '*/*',
'tenant-id': tenantId,
Authorization: 'Bearer ' + getAccessToken(),
},
formData: {
directory,
},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data);
if (result.error === 1) {
uni.showToast({
icon: 'none',
title: result.msg,
});
} else {
return resolve(result);
}
},
fail: (error) => {
console.log('上传失败:', error);
return resolve(false);
},
complete: () => {
uni.hideLoading();
},
});
});
},
// 获取文件预签名地址
getFilePresignedUrl: (name, directory) => {
return request({
url: '/infra/file/presigned-url',
method: 'GET',
params: {
name,
directory,
},
});
},
// 创建文件
createFile: (data) => {
return request({
url: '/infra/file/create', // 请求的 URL
method: 'POST', // 请求方法
data: data, // 要发送的数据
});
},
};
export default FileApi;

17
sheep/api/infra/tenant.js Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/sheep/request';
/**
* 通过网站域名获取租户信息
* @param {string} website - 网站域名
* @returns {Promise<Object>} 租户信息
*/
export function getTenantByWebsite(website) {
return request({
url: '/system/tenant/get-by-website',
method: 'GET',
params: { website },
custom: {
isToken: false, // 避免登录情况下,跨租户访问被拦截
},
});
}

View File

@@ -0,0 +1,5 @@
const AddressApi = {
// API methods have been removed as they are not needed
};
export default AddressApi;

132
sheep/api/member/auth.js Normal file
View File

@@ -0,0 +1,132 @@
import request from '@/sheep/request';
const AuthUtil = {
// 使用手机 + 密码登录
login: (data) => {
return request({
url: '/member/auth/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 使用手机 + 验证码登录
smsLogin: (data) => {
return request({
url: '/member/auth/sms-login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 发送手机验证码
sendSmsCode: (mobile, scene) => {
return request({
url: '/member/auth/send-sms-code',
method: 'POST',
data: {
mobile,
scene,
},
custom: {
loadingMsg: '发送中',
showSuccess: true,
successMsg: '发送成功',
},
});
},
// 登出系统
logout: () => {
return request({
url: '/member/auth/logout',
method: 'POST',
});
},
// 刷新令牌
refreshToken: (refreshToken) => {
return request({
url: '/member/auth/refresh-token',
method: 'POST',
params: {
refreshToken,
},
custom: {
showLoading: false, // 不用加载中
showError: false, // 不展示错误提示
},
});
},
// 社交授权的跳转
socialAuthRedirect: (type, redirectUri) => {
return request({
url: '/member/auth/social-auth-redirect',
method: 'GET',
params: {
type,
redirectUri,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 社交快捷登录
socialLogin: (type, code, state) => {
return request({
url: '/member/auth/social-login',
method: 'POST',
data: {
type,
code,
state,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 微信小程序的一键登录
weixinMiniAppLogin: (phoneCode, loginCode, state) => {
return request({
url: '/member/auth/weixin-mini-app-login',
method: 'POST',
data: {
phoneCode,
loginCode,
state,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
successMsg: '登录成功',
},
});
},
// 创建微信 JS SDK 初始化所需的签名
createWeixinMpJsapiSignature: (url) => {
return request({
url: '/member/auth/create-weixin-jsapi-signature',
method: 'POST',
params: {
url,
},
custom: {
showError: false,
showLoading: false,
},
});
},
//
};
export default AuthUtil;

View File

@@ -0,0 +1,37 @@
import request from '@/sheep/request';
const SignInApi = {
// 获得签到规则列表
getSignInConfigList: () => {
return request({
url: '/member/sign-in/config/list',
method: 'GET',
});
},
// 获得个人签到统计
getSignInRecordSummary: () => {
return request({
url: '/member/sign-in/record/get-summary',
method: 'GET',
});
},
// 签到
createSignInRecord: () => {
return request({
url: '/member/sign-in/record/create',
method: 'POST',
});
},
// 获得签到记录分页
getSignRecordPage: (params) => {
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/member/sign-in/record/page?${queryString}`,
method: 'GET',
});
},
};
export default SignInApi;

View File

@@ -0,0 +1,76 @@
import request from '@/sheep/request';
const SocialApi = {
// 获得社交用户
getSocialUser: (type) => {
return request({
url: '/member/social-user/get',
method: 'GET',
params: {
type
},
custom: {
showLoading: false,
},
});
},
// 社交绑定
socialBind: (type, code, state) => {
return request({
url: '/member/social-user/bind',
method: 'POST',
data: {
type,
code,
state
},
custom: {
custom: {
showSuccess: true,
loadingMsg: '绑定中',
successMsg: '绑定成功',
},
},
});
},
// 社交绑定
socialUnbind: (type, openid) => {
return request({
url: '/member/social-user/unbind',
method: 'DELETE',
data: {
type,
openid
},
custom: {
showLoading: false,
loadingMsg: '解除绑定',
successMsg: '解绑成功',
},
});
},
// 获取订阅消息模板列表
getSubscribeTemplateList: () =>
request({
url: '/member/social-user/get-subscribe-template-list',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
}),
// 获取微信小程序码
getWxaQrcode: async (path, query) => {
return await request({
url: '/member/social-user/wxa-qrcode',
method: 'POST',
data: {
scene: query,
path,
checkPath: false, // TODO 开发环境暂不检查 path 是否存在
},
});
},
};
export default SocialApi;

85
sheep/api/member/user.js Normal file
View File

@@ -0,0 +1,85 @@
import request from '@/sheep/request';
const UserApi = {
// 获得基本信息
getUserInfo: () => {
return request({
url: '/member/user/get',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 修改基本信息
updateUser: (data) => {
return request({
url: '/member/user/update',
method: 'PUT',
data,
custom: {
auth: true,
showSuccess: true,
successMsg: '更新成功'
},
});
},
// 修改用户手机
updateUserMobile: (data) => {
return request({
url: '/member/user/update-mobile',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 基于微信小程序的授权码,修改用户手机
updateUserMobileByWeixin: (code) => {
return request({
url: '/member/user/update-mobile-by-weixin',
method: 'PUT',
data: {
code
},
custom: {
showSuccess: true,
loadingMsg: '获取中',
successMsg: '修改成功'
},
});
},
// 修改密码
updateUserPassword: (data) => {
return request({
url: '/member/user/update-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 重置密码
resetUserPassword: (data) => {
return request({
url: '/member/user/reset-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
}
});
},
};
export default UserApi;

View File

@@ -0,0 +1,21 @@
import request from '@/sheep/request';
// TODO 芋艿:【直播】小程序直播还不支持
export default {
//小程序直播
mplive: {
getRoomList: (ids) =>
request({
url: 'app/mplive/getRoomList',
method: 'GET',
params: {
ids: ids.join(','),
},
}),
getMpLink: () =>
request({
url: 'app/mplive/getMpLink',
method: 'GET',
}),
},
};

View File

@@ -0,0 +1,18 @@
import request from '@/sheep/request';
export default {
// 苹果相关
apple: {
// 第三方登录
login: (data) =>
request({
url: 'third/apple/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
}),
},
};

13
sheep/api/system/area.js Normal file
View File

@@ -0,0 +1,13 @@
import request from '@/sheep/request';
const AreaApi = {
// 获得地区树
getAreaTree: () => {
return request({
url: '/system/area/tree',
method: 'GET'
});
},
};
export default AreaApi;

166
sheep/api/system/auth.js Normal file
View File

@@ -0,0 +1,166 @@
import request from '@/sheep/request';
const AuthUtil = {
// 使用用户名 + 密码登录
login: (data) => {
return request({
url: '/system/auth/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 使用手机 + 验证码登录
smsLogin: (data) => {
return request({
url: '/system/auth/sms-login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 发送手机验证码
sendSmsCode: (mobile, scene) => {
return request({
url: '/system/auth/send-sms-code',
method: 'POST',
data: {
mobile,
scene,
},
custom: {
loadingMsg: '发送中',
showSuccess: true,
successMsg: '发送成功',
},
});
},
// 登出系统
logout: () => {
return request({
url: '/system/auth/logout',
method: 'POST',
});
},
// 刷新令牌
refreshToken: (refreshToken) => {
return request({
url: '/system/auth/refresh-token',
method: 'POST',
params: {
refreshToken,
},
custom: {
showLoading: false, // 不用加载中
showError: false, // 不展示错误提示
},
});
},
// 社交授权的跳转
socialAuthRedirect: (type, redirectUri) => {
return request({
url: '/system/auth/social-auth-redirect',
method: 'GET',
params: {
type,
redirectUri,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 社交快捷登录
socialLogin: (type, code, state) => {
return request({
url: '/system/auth/social-login',
method: 'POST',
data: {
type,
code,
state,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 微信小程序的一键登录
weixinMiniAppLogin: (phoneCode, loginCode, state) => {
return request({
url: '/system/auth/weixin-mini-app-login',
method: 'POST',
data: {
phoneCode,
loginCode,
state,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
successMsg: '登录成功',
},
});
},
// 创建微信 JS SDK 初始化所需的签名
createWeixinMpJsapiSignature: (url) => {
return request({
url: '/system/auth/create-weixin-jsapi-signature',
method: 'POST',
params: {
url,
},
custom: {
showError: false,
showLoading: false,
},
});
},
// 获取用户权限信息
getInfo: () => {
return request({
url: '/system/auth/get-permission-info',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
});
},
// 获取验证图片以及 token
getCaptchaCode: (data) => {
return request({
url: '/system/captcha/get',
method: 'POST',
data,
custom: {
showError: false,
showLoading: false,
},
});
},
// 滑动或者点选验证
verifyCaptcha: (data) => {
return request({
url: '/system/captcha/check',
method: 'POST',
data,
custom: {
showError: false,
showLoading: false,
},
});
}
};
export default AuthUtil;

16
sheep/api/system/dict.js Normal file
View File

@@ -0,0 +1,16 @@
import request from '@/sheep/request';
const DictApi = {
// 根据字典类型查询字典数据信息
getDictDataListByType: (type) => {
return request({
url: `/system/dict-data/type`,
method: 'GET',
params: {
type,
},
});
},
};
export default DictApi;

View File

@@ -0,0 +1,76 @@
import request from '@/sheep/request';
const SocialApi = {
// 获得社交用户
getSocialUser: (type) => {
return request({
url: '/system/social-user/get',
method: 'GET',
params: {
type
},
custom: {
showLoading: false,
},
});
},
// 社交绑定
socialBind: (type, code, state) => {
return request({
url: '/system/social-user/bind',
method: 'POST',
data: {
type,
code,
state
},
custom: {
custom: {
showSuccess: true,
loadingMsg: '绑定中',
successMsg: '绑定成功',
},
},
});
},
// 社交解绑
socialUnbind: (type, openid) => {
return request({
url: '/system/social-user/unbind',
method: 'DELETE',
data: {
type,
openid
},
custom: {
showLoading: false,
loadingMsg: '解除绑定',
successMsg: '解绑成功',
},
});
},
// 获取订阅消息模板列表
getSubscribeTemplateList: () =>
request({
url: '/system/social-user/get-subscribe-template-list',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
}),
// 获取微信小程序码
getWxaQrcode: async (path, query) => {
return await request({
url: '/system/social-user/wxa-qrcode',
method: 'POST',
data: {
scene: query,
path,
checkPath: false, // TODO 开发环境暂不检查 path 是否存在
},
});
},
};
export default SocialApi;

97
sheep/api/system/user.js Normal file
View File

@@ -0,0 +1,97 @@
import request from '@/sheep/request';
const UserApi = {
// 获得基本信息
getUserInfo: () => {
return request({
url: '/system/user/profile/get',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 修改基本信息
updateUser: (data) => {
return request({
url: '/system/user/profile/update',
method: 'PUT',
data,
custom: {
auth: true,
showSuccess: true,
successMsg: '更新成功'
},
});
},
// 修改用户手机
updateUserMobile: (data) => {
return request({
url: '/system/user/profile/update-mobile',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 基于微信小程序的授权码,修改用户手机
updateUserMobileByWeixin: (code) => {
return request({
url: '/system/user/profile/update-mobile-by-weixin',
method: 'PUT',
data: {
code
},
custom: {
showSuccess: true,
loadingMsg: '获取中',
successMsg: '修改成功'
},
});
},
// 修改密码
updateUserPassword: (data) => {
return request({
url: '/system/user/profile/update-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 重置密码
resetUserPassword: (data) => {
return request({
url: '/system/auth/reset-password',
method: 'POST',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
}
});
},
// 上传用户头像
uploadAvatar: (data) => {
return request({
url: '/system/user/profile/update-avatar',
method: 'PUT',
data,
custom: {
loadingMsg: '上传中',
showSuccess: true,
successMsg: '上传成功'
},
});
}
};
export default UserApi;

View File

@@ -0,0 +1,233 @@
<template>
<view v-if="dialogStore.show" class="company-dept-dialog">
<view class="company-dept-dialog__mask" @tap="handleCancel" />
<view class="company-dept-dialog__panel">
<view class="company-dept-dialog__header">
<text class="company-dept-dialog__title">{{ dialogStore.title }}</text>
</view>
<view class="company-dept-dialog__body">
<view class="company-dept-dialog__field">
<text class="company-dept-dialog__label">公司</text>
<picker
mode="selector"
:range="companyOptions"
range-key="companyName"
:value="companyIndex"
@change="onCompanyChange"
>
<view class="company-dept-dialog__picker">
<text class="company-dept-dialog__picker-text">
{{ currentCompanyName }}
</text>
<text class="company-dept-dialog__arrow"></text>
</view>
</picker>
</view>
<view class="company-dept-dialog__field">
<text class="company-dept-dialog__label">部门</text>
<picker
mode="selector"
:range="deptOptions"
range-key="deptName"
:value="deptIndex"
@change="onDeptChange"
>
<view class="company-dept-dialog__picker">
<text class="company-dept-dialog__picker-text">
{{ currentDeptName }}
</text>
<text class="company-dept-dialog__arrow"></text>
</view>
</picker>
</view>
</view>
<view class="company-dept-dialog__footer">
<button class="company-dept-dialog__btn company-dept-dialog__btn--cancel" @tap="handleCancel">
取消
</button>
<button
class="company-dept-dialog__btn company-dept-dialog__btn--confirm"
:class="{ 'company-dept-dialog__btn--disabled': !isConfirmEnabled }"
:disabled="!isConfirmEnabled"
@tap="handleConfirm"
>
确定
</button>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
import sheep from '@/sheep';
const dialogStore = sheep.$store('company-dept');
const companyOptions = computed(() => dialogStore.companyList || []);
const deptOptions = computed(() => dialogStore.getDeptsByCompanyId(dialogStore.selectedCompanyId));
const companyIndex = computed(() => {
const index = companyOptions.value.findIndex((item) => item.companyId === dialogStore.selectedCompanyId);
return index >= 0 ? index : 0;
});
const deptIndex = computed(() => {
const index = deptOptions.value.findIndex((item) => item.deptId === dialogStore.selectedDeptId);
return index >= 0 ? index : 0;
});
const currentCompanyName = computed(() => {
if (!companyOptions.value.length) return '暂无公司可选';
const company = companyOptions.value.find((item) => item.companyId === dialogStore.selectedCompanyId);
return company?.companyName || '请选择公司';
});
const currentDeptName = computed(() => {
if (!deptOptions.value.length) return '暂无部门可选';
const dept = deptOptions.value.find((item) => item.deptId === dialogStore.selectedDeptId);
return dept?.deptName || '请选择部门';
});
const isConfirmEnabled = computed(() => !!dialogStore.selectedCompanyId && !!dialogStore.selectedDeptId);
const onCompanyChange = (event) => {
const index = event?.detail?.value;
if (index === undefined) return;
const company = companyOptions.value[index];
if (company) {
dialogStore.setSelectedCompany(company.companyId);
}
};
const onDeptChange = (event) => {
const index = event?.detail?.value;
if (index === undefined) return;
const dept = deptOptions.value[index];
if (dept) {
dialogStore.setSelectedDept(dept.deptId);
}
};
const handleCancel = () => {
dialogStore.cancel();
};
const handleConfirm = () => {
if (!isConfirmEnabled.value) return;
dialogStore.confirm();
};
</script>
<style scoped lang="scss">
.company-dept-dialog {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
&__mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.45);
}
&__panel {
position: relative;
width: 620rpx;
background: #ffffff;
border-radius: 24rpx;
padding: 48rpx 40rpx 32rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.18);
}
&__header {
margin-bottom: 40rpx;
}
&__title {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #1f1f1f;
text-align: center;
line-height: 1.4;
}
&__body {
display: flex;
flex-direction: column;
gap: 32rpx;
}
&__field {
display: flex;
flex-direction: column;
gap: 16rpx;
}
&__label {
font-size: 28rpx;
color: #606266;
}
&__picker {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 28rpx;
border-radius: 16rpx;
background: #f7f8fa;
border: 1rpx solid #e4e7ed;
}
&__picker-text {
font-size: 28rpx;
color: #303133;
}
&__arrow {
font-size: 24rpx;
color: #909399;
}
&__footer {
display: flex;
justify-content: space-between;
gap: 24rpx;
margin-top: 48rpx;
}
&__btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 500;
}
&__btn--cancel {
color: #606266;
background: #f5f7fa;
border: 1rpx solid transparent;
}
&__btn--confirm {
color: #ffffff;
background: linear-gradient(90deg, #2f6bff 0%, #3c8bff 100%);
border: 1rpx solid transparent;
}
&__btn--disabled {
opacity: 0.5;
}
}
</style>

View File

@@ -0,0 +1,145 @@
<!-- 绑定/更换手机号 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"c
: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 { getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/system/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();
// 绑定成功后返回上一页或首页
setTimeout(() => {
if (getCurrentPages().length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: '/pages/index/menu'
});
}
}, 500);
}
// 使用微信手机号
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();
// 绑定成功后返回上一页或首页
setTimeout(() => {
if (getCurrentPages().length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: '/pages/index/menu'
});
}
}, 500);
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,125 @@
<!-- 修改密码登录时 -->
<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="changePasswordRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
type="number"
maxlength="4"
: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('changePassword')"
>
{{ getSmsTimer('resetPassword') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="reNewPassword" 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="changePasswordSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<button class="ss-reset-button type-btn" @tap="goBack">
取消修改
</button>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import { code, password } from '@/sheep/validate/form';
import { getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/system/user';
const changePasswordRef = ref(null);
// 数据
const state = reactive({
model: {
mobile: '', // 手机号
code: '', // 验证码
password: '', // 密码
},
rules: {
code,
password,
},
});
// 返回上一页
function goBack() {
if (getCurrentPages().length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: '/pages/index/menu'
});
}
}
// 更改密码
async function changePasswordSubmit() {
// 参数校验
const validate = await unref(changePasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 发起请求
const { code } = await UserApi.updateUserPassword(state.model);
if (code !== 0) {
return;
}
// 成功后返回上一页或首页
setTimeout(() => {
if (getCurrentPages().length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: '/pages/index/menu'
});
}
}, 500);
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,160 @@
<!-- 微信授权信息 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 FileApi from '@/sheep/api/infra/file';
import UserApi from '@/sheep/api/system/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();
// 授权成功后返回上一页或首页
setTimeout(() => {
if (getCurrentPages().length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: '/pages/index/menu'
});
}
}, 500);
}
}
</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,126 @@
<!-- 重置密码未登录时 -->
<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="goToLogin">
返回登录
</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 { getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/system/user';
// 跳转到登录页
function goToLogin() {
uni.navigateTo({
url: '/pages/login/index'
});
}
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;
}
// 成功后,用户重新登录
goToLogin();
};
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,430 @@
<!-- 统一登录组件 - 整合账号密码登录和短信登录 -->
<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"
:class="{ 'head-title-active': loginType === 'account', 'head-title-animation': loginType === 'account' }"
@tap="switchLoginType('account')"
>
账号登录
</view>
<!-- <view
class="head-title"
:class="{ 'head-title-active': loginType === 'sms', 'head-title-animation': loginType === 'sms' }"
@tap="switchLoginType('sms')"
>
短信登录
</view> -->
</view>
<!-- 副标题 -->
<view class="head-subtitle">
{{ loginType === 'account' ? '如果未设置过密码,请点击忘记密码' : '未注册的手机号,验证后自动注册账号' }}
</view>
</view>
<!-- 账号密码登录表单 -->
<uni-forms
v-if="loginType === 'account'"
ref="accountLoginRef"
v-model="accountState.model"
:rules="accountState.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="username" label="账号">
<uni-easyinput placeholder="请输入账号" v-model="accountState.model.username" :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="accountState.model.password"
:inputBorder="false"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="handleAccountLogin">登录</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<!-- 短信登录表单 -->
<uni-forms
v-if="loginType === 'sms'"
ref="smsLoginRef"
v-model="smsState.model"
:rules="smsState.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="smsState.model.mobile"
:inputBorder="false"
type="number"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn code-btn-start"
:disabled="!canSendSms"
:class="{ 'code-btn-end': !canSendSms }"
@tap="checkAgreementAndGetSmsCode"
>
{{ smsTimer }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="smsState.model.code"
:inputBorder="false"
type="number"
maxlength="4"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="handleSmsLogin">登录</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<!-- 验证码弹窗 -->
<s-verify
ref="verifyRef"
:captcha-type="captchaType"
mode="pop"
@success="onCaptchaSuccess"
@error="onCaptchaError"
/>
</view>
</template>
<script setup>
import { ref, reactive, unref, computed, onUnmounted } from 'vue';
import sheep from '@/sheep';
import { username, password, mobile, code } from '@/sheep/validate/form';
import { showAuthModal, getSmsCode, useSmsTimer } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/system/auth';
import SVerify from '@/sheep/components/s-verify/s-verify.vue';
import { navigateAfterLogin } from '@/sheep/helper/login-redirect';
const accountLoginRef = ref(null);
const smsLoginRef = ref(null);
const verifyRef = ref(null);
const emits = defineEmits(['onConfirm']);
const props = defineProps({
agreeStatus: {
type: [Boolean, null],
default: null,
},
});
// 登录方式account(账号) 或 sms(短信)
const loginType = ref('account');
// 验证码相关
const appStore = sheep.$store('app');
const captchaType = ref('blockPuzzle');
const captchaEnable = computed(() => appStore.captchaEnable !== false);
const pendingLoginData = ref(null);
// 短信验证码计时器
const { timer: smsTimer, cleanup } = useSmsTimer('smsLogin');
// 组件卸载时清理定时器
onUnmounted(() => {
cleanup();
});
// 账号登录数据
const accountState = reactive({
model: {
username: '', // 账号
password: '', // 密码
captchaCode: '', // 图形验证码
captchaVerification: '', // 验证码验证结果
},
rules: {
username,
password,
},
});
// 短信登录数据
const smsState = reactive({
model: {
mobile: '', // 手机号
code: '', // 验证码
captchaVerification: '', // 验证码验证结果
},
rules: {
mobile,
code,
},
});
// 是否可以发送短信验证码
const canSendSms = computed(() => {
return smsTimer.value === '获取验证码' && props.agreeStatus === true;
});
// 切换登录方式
function switchLoginType(type) {
loginType.value = type;
}
// 显示验证码弹窗
function showCaptcha() {
if (verifyRef.value) {
verifyRef.value.show();
}
}
// 验证码验证成功回调
function onCaptchaSuccess(data) {
if (pendingLoginData.value) {
pendingLoginData.value.captchaVerification = data.captchaVerification;
if (loginType.value === 'account') {
accountState.model.captchaVerification = data.captchaVerification;
} else {
smsState.model.captchaVerification = data.captchaVerification;
}
// 执行待定的登录请求
if (loginType.value === 'account') {
performAccountLogin(pendingLoginData.value);
} else {
performSmsLogin(pendingLoginData.value);
}
pendingLoginData.value = null;
}
}
// 验证码验证失败回调
function onCaptchaError(error) {
sheep.$helper.toast(error?.message || '验证码验证失败');
pendingLoginData.value = null;
}
// 处理账号登录点击
async function handleAccountLogin() {
// 表单验证
const validate = await unref(accountLoginRef)
.validate()
.catch((error) => {
console.log('表单验证失败: ', error);
});
if (!validate) return;
accountState.model.captchaVerification = '';
// 检查协议状态
if (props.agreeStatus !== true) {
emits('onConfirm', true);
sheep.$helper.toast('请先勾选协议');
return;
}
// 如果启用验证码,先显示验证码
if (captchaEnable.value) {
pendingLoginData.value = { ...accountState.model };
showCaptcha();
} else {
// 直接登录
performAccountLogin(accountState.model);
}
}
// 执行账号登录
async function performAccountLogin(loginData) {
try {
const { code } = await AuthUtil.login(loginData);
if (code === 0) {
sheep.$helper.toast('登录成功');
navigateAfterLogin();
}
} catch (error) {
sheep.$helper.toast(error.message || '登录失败');
} finally {
accountState.model.captchaVerification = '';
}
}
// 处理短信登录点击
async function handleSmsLogin() {
// 表单验证
const validate = await unref(smsLoginRef)
.validate()
.catch((error) => {
console.log('表单验证失败: ', error);
});
if (!validate) return;
smsState.model.captchaVerification = '';
// 检查协议状态
if (props.agreeStatus !== true) {
emits('onConfirm', true);
sheep.$helper.toast('请先勾选协议');
return;
}
// 如果启用验证码,先显示验证码
if (captchaEnable.value) {
pendingLoginData.value = { ...smsState.model };
showCaptcha();
} else {
// 直接登录
performSmsLogin(smsState.model);
}
}
// 执行短信登录
async function performSmsLogin(loginData) {
try {
const { code } = await AuthUtil.smsLogin(loginData);
if (code === 0) {
sheep.$helper.toast('登录成功');
navigateAfterLogin();
}
} catch (error) {
sheep.$helper.toast(error.message || '登录失败');
} finally {
smsState.model.captchaVerification = '';
}
}
// 检查协议并获取短信验证码
async function checkAgreementAndGetSmsCode() {
// 检查协议状态
if (props.agreeStatus !== true) {
emits('onConfirm', true);
sheep.$helper.toast('请先勾选协议');
return;
}
// 使用现有的getSmsCode函数
getSmsCode('smsLogin', smsState.model.mobile);
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
// 标题样式增强
.head-title {
position: relative;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
color: var(--ui-BG-Main);
}
}
.head-title-active {
color: var(--ui-BG-Main);
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 2px;
background: var(--ui-BG-Main);
border-radius: 1px;
}
}
.head-title-animation {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0.6;
}
to {
opacity: 1;
}
}
// 覆盖登录按钮样式,使用主题色
.login-btn-start {
background: var(--ui-BG-Main) !important;
border: none !important;
&:hover {
background: var(--ui-BG-Main-1) !important;
}
}
// 覆盖验证码按钮样式,使用主题色
.code-btn-start {
border: 2rpx solid var(--ui-BG-Main) !important;
color: var(--ui-BG-Main) !important;
&:hover {
background: var(--ui-BG-Main-tag) !important;
}
}
// 覆盖协议链接样式,使用主题色
:deep(.tcp-text) {
color: var(--ui-BG-Main) !important;
}
// 确保表单输入框焦点状态也使用主题色
:deep(.uni-easyinput__content-input) {
&:focus {
border-color: var(--ui-BG-Main) !important;
}
}
// 确保uni-forms的错误提示等也使用合适的颜色
:deep(.uni-forms-item__error) {
color: #ff4d4f !important;
}
// 按钮悬停状态优化
.login-btn-start {
transition: all 0.3s ease !important;
&:active {
background: var(--ui-BG-Main-1) !important;
transform: scale(0.98);
}
}
.code-btn-start {
transition: all 0.3s ease !important;
&:active {
background: var(--ui-BG-Main-tag) !important;
transform: scale(0.98);
}
}
</style>

View File

@@ -0,0 +1,154 @@
@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;
line-height: 36rpx;
}
.head-title-active {
width: 160rpx;
font-size: 32rpx;
font-weight: 600;
color: #999;
line-height: 36rpx;
}
.head-title-animation {
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;
text-align: left;
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, #0055A2) !important;
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 400;
color: var(--ui-BG-Main, #0055A2) !important;
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: var(--ui-BG-Main, #0055A2) !important;
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 500;
color: #fff !important;
border: none;
position: relative;
z-index: 1;
}
.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, #0055A2) !important;
}
}
}
// 修改密码
.editPwd-btn-box {
.save-btn {
width: 690rpx;
line-height: 70rpx;
background: var(--ui-BG-Main, #0055A2) !important;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: #ffffff !important;
}
.forgot-btn {
width: 690rpx;
line-height: 70rpx;
font-size: 28rpx;
font-weight: 500;
color: #999999;
}
}

View File

@@ -0,0 +1,203 @@
<!--
默认头像组件
参考 zt-vue-element CropperAvatar 实现
支持默认头像逻辑头像URL优先否则显示用户昵称首字母
-->
<template>
<view class="avatar-container" @click="handleClick">
<!-- 有头像时显示图片 -->
<image
v-if="avatarUrl"
:src="avatarUrl"
:style="avatarStyle"
class="avatar-image"
mode="aspectFill"
@error="handleImageError"
/>
<!-- 无头像时显示首字母头像 -->
<view
v-else
class="avatar-initial"
:style="[avatarStyle, initialStyle]"
>
{{ initialText }}
</view>
<!-- 编辑按钮 -->
<view v-if="editable && !readonly" class="edit-mask">
<text class="edit-icon">+</text>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
// 组件属性
const props = defineProps({
// 头像URL
src: {
type: String,
default: ''
},
// 用户昵称,用于生成首字母
nickname: {
type: String,
default: ''
},
// 头像尺寸
size: {
type: [String, Number],
default: 80
},
// 是否可编辑
editable: {
type: Boolean,
default: false
},
// 是否只读
readonly: {
type: Boolean,
default: false
},
// 背景色(用于首字母头像)
bgColor: {
type: String,
default: '#0055A2'
},
// 文字色(用于首字母头像)
textColor: {
type: String,
default: '#ffffff'
},
// 自定义初始字符
initial: {
type: String,
default: ''
}
})
// 事件定义
const emit = defineEmits(['click', 'error'])
// 响应式数据
const avatarUrl = ref(props.src)
const imageError = ref(false)
// 计算属性
const avatarStyle = computed(() => {
const size = typeof props.size === 'number' ? `${props.size}rpx` : props.size
return {
width: size,
height: size,
borderRadius: '50%'
}
})
const initialStyle = computed(() => ({
backgroundColor: props.bgColor,
color: props.textColor,
fontSize: `${Math.floor(props.size * 0.4)}rpx`,
fontWeight: 'bold'
}))
// 生成初始字符逻辑
const initialText = computed(() => {
// 如果提供了自定义初始字符,使用它
if (props.initial) {
return props.initial.charAt(0).toUpperCase()
}
// 如果有昵称,取首字母
if (props.nickname) {
// 处理中文和英文
const firstChar = props.nickname.charAt(0)
// 如果是中文,直接使用
if (/[\u4e00-\u9fa5]/.test(firstChar)) {
return firstChar
}
// 如果是英文,转大写
return firstChar.toUpperCase()
}
// 默认返回用户图标
return '用'
})
// 监听src变化
watch(() => props.src, (newSrc) => {
avatarUrl.value = newSrc
imageError.value = false
}, { immediate: true })
// 方法
const handleClick = () => {
if (!props.readonly) {
emit('click')
}
}
const handleImageError = () => {
imageError.value = true
avatarUrl.value = '' // 清空URL显示首字母头像
emit('error')
}
// 暴露方法给父组件
defineExpose({
refresh: () => {
avatarUrl.value = props.src
imageError.value = false
}
})
</script>
<style lang="scss" scoped>
.avatar-container {
position: relative;
display: inline-block;
overflow: hidden;
&:hover .edit-mask {
opacity: 1;
}
}
.avatar-image {
display: block;
object-fit: cover;
}
.avatar-initial {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--theme-primary, #0055A2);
color: #ffffff;
font-weight: bold;
text-align: center;
user-select: none;
}
.edit-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.edit-icon {
color: #ffffff;
font-size: 48rpx;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,173 @@
<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,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,273 @@
<template>
<view
class="page-app"
:class="['theme-' + sys?.mode, 'main-' + sys?.theme, 'font-' + sys?.fontSize]"
>
<view class="page-main" :style="[bgMain]">
<!-- &lt;!&ndash; 顶部导航栏-情况1默认通用顶部导航栏 &ndash;&gt;-->
<!-- <su-navbar-->
<!-- v-if="navbar === 'normal'"-->
<!-- :title="title"-->
<!-- statusBar-->
<!-- :color="color"-->
<!-- :tools="tools"-->
<!-- :opacityBgUi="opacityBgUi"-->
<!-- @search="(e) => emits('search', e)"-->
<!-- :defaultSearch="defaultSearch"-->
<!-- />-->
<!-- &lt;!&ndash; 顶部导航栏-情况2装修组件导航栏-标准 &ndash;&gt;-->
<!-- <s-custom-navbar-->
<!-- v-else-if="navbar === 'custom' && navbarMode === 'normal'"-->
<!-- :data="navbarStyle"-->
<!-- :showLeftButton="showLeftButton"-->
<!-- />-->
<view class="page-body" :style="[bgBody]">
<!-- &lt;!&ndash; 顶部导航栏-情况3沉浸式头部 &ndash;&gt;-->
<!-- <su-inner-navbar v-if="navbar === 'inner'" :title="title" />-->
<!-- <view-->
<!-- v-if="navbar === 'inner'"-->
<!-- :style="[{ paddingTop: sheep?.$platform?.navbar + 'px' }]"-->
<!-- ></view>-->
<!-- &lt;!&ndash; 顶部导航栏-情况4装修组件导航栏-沉浸式 &ndash;&gt;-->
<!-- <s-custom-navbar-->
<!-- v-if="navbar === 'custom' && navbarMode === 'inner'"-->
<!-- :data="navbarStyle"-->
<!-- :showLeftButton="showLeftButton"-->
<!-- />-->
<!-- 页面内容插槽 -->
<slot />
<!-- 底部导航 -->
<s-tabbar-uview v-if="tabbar !== ''" :path="tabbar" />
</view>
</view>
<view class="page-modal">
<!-- 全局快捷入口 -->
<!-- <s-menu-tools /> -->
<CompanyDeptDialog />
</view>
</view>
</template>
<script setup>
/**
* 模板组件 - 提供页面公共组件,属性,方法
*/
import { computed, onMounted } from 'vue';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
// #ifdef MP-WEIXIN
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
// #endif
// 引入新的基于 uview-plus 的底部导航组件
import STabbarUview from '@/sheep/components/s-tabbar-uview/s-tabbar-uview.vue';
import CompanyDeptDialog from '@/sheep/components/company-dept-dialog/company-dept-dialog.vue';
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 || props.bgStyle.color} 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 || props.bgStyle.color} url(${sheep.$url.cdn(
props.bgStyle.backgroundImage,
)}) no-repeat top center / 100% auto`,
};
}
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
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline'],
});
// 微信小程序分享好友
onShareAppMessage(() => {
return {
title: shareInfo.value.title,
path: shareInfo.value.forward.path,
imageUrl: shareInfo.value.image,
};
});
// 微信小程序分享朋友圈
onShareTimeline(() => {
return {
title: shareInfo.value.title,
query: shareInfo.value.forward.path,
imageUrl: shareInfo.value.image,
};
});
// #endif
// 组件中使用 onMounted 监听页面加载,不是页面组件不使用 onShow
onMounted(()=>{
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;
}
}
.page-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
pointer-events: none;
> * {
pointer-events: auto;
}
}
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<view class="tabbar-container" v-if="showTabbar">
<u-tabbar
:value="currentPath"
:fixed="true"
:placeholder="true"
:safeAreaInsetBottom="true"
:border="true"
:activeColor="activeColor"
:inactiveColor="inactiveColor"
:bgColor="bgColor"
@change="handleTabbarChange"
>
<u-tabbar-item
v-for="(item, index) in tabbarList"
:key="index"
:name="item.pagePath"
:text="item.text"
>
<template #active-icon>
<view class="custom-icon active">
<u-icon
:name="item.activeIcon || item.icon"
size="24"
:color="activeColor"
/>
</view>
</template>
<template #inactive-icon>
<view class="custom-icon">
<u-icon
:name="item.icon"
size="24"
:color="inactiveColor"
/>
</view>
</template>
</u-tabbar-item>
</u-tabbar>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
path: {
type: String,
default: ''
}
})
// 底部导航配置
const tabbarList = ref([
{
pagePath: '/pages/index/menu',
text: '菜单',
icon: 'home',
activeIcon: 'home-fill'
},
{
pagePath: '/pages/index/user',
text: '我的',
icon: 'account',
activeIcon: 'account-fill'
}
])
// 颜色配置
const activeColor = '#0055A2'
const inactiveColor = '#999999'
const bgColor = '#ffffff'
// 当前路径
const currentPath = computed(() => {
return props.path || getCurrentPath()
})
// 是否显示tabbar
const showTabbar = computed(() => {
const currentRoute = getCurrentPath()
return tabbarList.value.some(item => item.pagePath === currentRoute)
})
// 获取当前页面路径
const getCurrentPath = () => {
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
return '/' + currentPage.route
}
return ''
}
// 处理tabbar切换
const handleTabbarChange = (name) => {
if (name !== currentPath.value) {
uni.switchTab({
url: name,
fail: (error) => {
console.error('switchTab failed:', error)
// 如果switchTab失败使用普通跳转
uni.navigateTo({
url: name,
fail: (navError) => {
console.error('navigateTo failed:', navError)
}
})
}
})
}
}
onMounted(() => {
// 隐藏原生tabBar
uni.hideTabBar({
animation: false
})
})
</script>
<style lang="scss" scoped>
.tabbar-container {
position: relative;
z-index: 999;
}
.custom-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
transition: transform 0.2s ease;
&.active {
transform: scale(1.1);
}
}
:deep(.u-tabbar) {
background-color: #ffffff;
border-top: 1px solid #ebedf0;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
.u-tabbar-item {
padding: 4px 0 8px;
&__text {
font-size: 10px;
margin-top: 4px;
font-weight: 500;
line-height: 1.2;
}
&__icon {
margin-bottom: 0;
}
}
}
// 为不同主题色适配
:deep(.u-tabbar .u-tabbar-item--active .u-tabbar-item__text) {
color: #0055A2;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,304 @@
'use strict';
import FileApi from '@/sheep/api/infra/file';
const ERR_MSG_OK = 'chooseAndUploadFile:ok';
const ERR_MSG_FAIL = 'chooseAndUploadFile:fail';
function chooseImage(opts) {
const {
count,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
extension,
} = opts;
return new Promise((resolve, reject) => {
uni.chooseImage({
count,
sizeType,
sourceType,
extension,
success(res) {
resolve(normalizeChooseAndUploadFileRes(res, 'image'));
},
fail(res) {
reject({
errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL),
});
},
});
});
}
function chooseVideo(opts) {
const { camera, compressed, maxDuration, sourceType = ['album', 'camera'], extension } = opts;
return new Promise((resolve, reject) => {
uni.chooseVideo({
camera,
compressed,
maxDuration,
sourceType,
extension,
success(res) {
const { tempFilePath, duration, size, height, width } = res;
resolve(
normalizeChooseAndUploadFileRes(
{
errMsg: 'chooseVideo:ok',
tempFilePaths: [tempFilePath],
tempFiles: [
{
name: (res.tempFile && res.tempFile.name) || '',
path: tempFilePath,
size,
type: (res.tempFile && res.tempFile.type) || '',
width,
height,
duration,
fileType: 'video',
cloudPath: '',
},
],
},
'video',
),
);
},
fail(res) {
reject({
errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL),
});
},
});
});
}
function chooseAll(opts) {
const { count, extension } = opts;
return new Promise((resolve, reject) => {
let chooseFile = uni.chooseFile;
if (typeof wx !== 'undefined' && typeof wx.chooseMessageFile === 'function') {
chooseFile = wx.chooseMessageFile;
}
if (typeof chooseFile !== 'function') {
return reject({
errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。',
});
}
chooseFile({
type: 'all',
count,
extension,
success(res) {
resolve(normalizeChooseAndUploadFileRes(res));
},
fail(res) {
reject({
errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL),
});
},
});
});
}
function normalizeChooseAndUploadFileRes(res, fileType) {
res.tempFiles.forEach((item, index) => {
if (!item.name) {
item.name = item.path.substring(item.path.lastIndexOf('/') + 1);
}
if (fileType) {
item.fileType = fileType;
}
item.cloudPath = Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.'));
});
if (!res.tempFilePaths) {
res.tempFilePaths = res.tempFiles.map((file) => file.path);
}
return res;
}
async function readFile(uniFile) {
// 微信小程序
if (uni.getFileSystemManager) {
const fs = uni.getFileSystemManager();
return fs.readFileSync(uniFile.path);
}
// H5 等
return uniFile.arrayBuffer();
}
function uploadCloudFiles(files, max = 5, onUploadProgress) {
files = JSON.parse(JSON.stringify(files));
const len = files.length;
let count = 0;
let self = this;
return new Promise((resolve) => {
while (count < max) {
next();
}
function next() {
let cur = count++;
if (cur >= len) {
!files.find((item) => !item.url && !item.errMsg) && resolve(files);
return;
}
const fileItem = files[cur];
const index = self.files.findIndex((v) => v.uuid === fileItem.uuid);
fileItem.url = '';
delete fileItem.errMsg;
uniCloud
.uploadFile({
filePath: fileItem.path,
cloudPath: fileItem.cloudPath,
fileType: fileItem.fileType,
onUploadProgress: (res) => {
res.index = index;
onUploadProgress && onUploadProgress(res);
},
})
.then((res) => {
fileItem.url = res.fileID;
fileItem.index = index;
if (cur < len) {
next();
}
})
.catch((res) => {
fileItem.errMsg = res.errMsg || res.message;
fileItem.index = index;
if (cur < len) {
next();
}
});
}
});
}
function uploadFilesFromPath(path, directory = '') {
// 目的:用于微信小程序,选择图片时,只有 path
return uploadFiles(
Promise.resolve({
tempFiles: [
{
path,
type: 'image/jpeg',
name: path.includes('/') ? path.substring(path.lastIndexOf('/') + 1) : path,
},
],
}),
{
directory,
},
);
}
async function uploadFiles(choosePromise, { onChooseFile, onUploadProgress, directory }) {
// 获取选择的文件
const res = await choosePromise;
// 处理文件选择回调
let files = res.tempFiles || [];
if (onChooseFile) {
const customChooseRes = onChooseFile(res);
if (typeof customChooseRes !== 'undefined') {
files = await Promise.resolve(customChooseRes);
if (typeof files === 'undefined') {
files = res.tempFiles || []; // Fallback
}
}
}
// 如果是前端直连上传
if (UPLOAD_TYPE.CLIENT === import.meta.env.SHOPRO_UPLOAD_TYPE) {
// 为上传创建一组 Promise
const uploadPromises = files.map(async (file) => {
try {
// 1.1 获取文件预签名地址
const { data: presignedInfo } = await FileApi.getFilePresignedUrl(file.name, directory);
// 1.2 获取二进制文件对象
const fileBuffer = await readFile(file);
// 返回上传的 Promise
return new Promise((resolve, reject) => {
// 1.3. 上传文件到 S3
uni.request({
url: presignedInfo.uploadUrl,
method: 'PUT',
header: {
'Content-Type': file.type,
},
data: fileBuffer,
success: (res) => {
// 1.4. 记录文件信息到后端(异步)
createFile(presignedInfo, file);
// 1.5. 重新赋值
file.url = presignedInfo.url;
resolve(file);
},
fail: (err) => {
reject(err);
},
});
});
} catch (error) {
console.error('上传失败:', error);
throw error;
}
});
// 等待所有上传完成
return await Promise.all(uploadPromises); // 返回已上传的文件列表
} else {
// 后端上传
for (let file of files) {
const { data } = await FileApi.uploadFile(file.path, directory);
file.url = data;
}
return files;
}
}
function chooseAndUploadFile(
opts = {
type: 'all',
directory: undefined,
},
) {
if (opts.type === 'image') {
return uploadFiles(chooseImage(opts), opts);
} else if (opts.type === 'video') {
return uploadFiles(chooseVideo(opts), opts);
}
return uploadFiles(chooseAll(opts), opts);
}
/**
* 创建文件信息
* @param vo 文件预签名信息
* @param file 文件
*/
function createFile(vo, file) {
const fileVo = {
configId: vo.configId,
url: vo.url,
path: vo.path,
name: file.name,
type: file.fileType,
size: file.size,
};
FileApi.createFile(fileVo);
return fileVo;
}
/**
* 上传类型
*/
const UPLOAD_TYPE = {
// 客户端直接上传只支持S3服务
CLIENT: 'client',
// 客户端发送到后端上传
SERVER: 'server',
};
export { chooseAndUploadFile, uploadCloudFiles, uploadFilesFromPath };

View File

@@ -0,0 +1,677 @@
<!-- 文件上传基于 upload-file upload-image 实现 -->
<template>
<view class="uni-file-picker">
<view v-if="title" class="uni-file-picker__header">
<text class="file-title">{{ title }}</text>
<text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
</view>
<view v-if="subtitle" class="file-subtitle">
<view>{{ subtitle }}</view>
</view>
<upload-image
v-if="fileMediatype === 'image' && showType === 'grid'"
:readonly="readonly"
:image-styles="imageStyles"
:files-list="url"
:limit="limitLength"
:disablePreview="disablePreview"
:delIcon="delIcon"
@uploadFiles="uploadFiles"
@choose="choose"
@delFile="delFile"
>
<slot>
<view class="is-add">
<image :src="imgsrc" class="add-icon"></image>
</view>
</slot>
</upload-image>
<upload-file
v-if="fileMediatype !== 'image' || showType !== 'grid'"
:readonly="readonly"
:list-styles="listStyles"
:files-list="filesList"
:showType="showType"
:delIcon="delIcon"
@uploadFiles="uploadFiles"
@choose="choose"
@delFile="delFile"
>
<slot><button type="primary" size="mini">选择文件</button></slot>
</upload-file>
</view>
</template>
<script>
import { chooseAndUploadFile, uploadCloudFiles } from './choose-and-upload-file.js';
import { get_extname, get_files_and_is_max, get_file_data } from './utils.js';
import uploadImage from './upload-image.vue';
import uploadFile from './upload-file.vue';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
let fileInput = null;
/**
* FilePicker 文件选择上传
* @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
* @tutorial https://ext.dcloud.net.cn/plugin?id=4079
* @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定
* @property {String|Array} url url数据
* @property {Boolean} disabled = [true|false] 组件禁用
* @value true 禁用
* @value false 取消禁用
* @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮
* @value true 只读
* @value false 取消只读
* @property {Boolean} disable-preview = [true|false] 禁用图片预览,仅 mode:grid 时生效
* @value true 禁用图片预览
* @value false 取消禁用图片预览
* @property {Boolean} del-icon = [true|false] 是否显示删除按钮
* @value true 显示删除按钮
* @value false 不显示删除按钮
* @property {Boolean} auto-upload = [true|false] 是否自动上传值为true则只触发@select,可自行上传
* @value true 自动上传
* @value false 取消自动上传
* @property {Number|String} limit 最大选择个数 h5 会自动忽略多选的部分
* @property {String} title 组件标题,右侧显示上传计数
* @property {String} mode = [list|grid] 选择文件后的文件列表样式
* @value list 列表显示
* @value grid 宫格显示
* @property {String} file-mediatype = [image|video|all] 选择文件类型
* @value image 只选择图片
* @value video 只选择视频
* @value all 选择所有文件
* @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同
* @property {Object} list-style mode:list 时的样式
* @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同
* @event {Function} select 选择文件后触发
* @event {Function} progress 文件上传时触发
* @event {Function} success 上传成功触发
* @event {Function} fail 上传失败触发
* @event {Function} delete 文件从列表移除时触发
*/
export default {
name: 'sUploader',
components: {
uploadImage,
uploadFile,
},
options: {
virtualHost: true,
},
emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'update:url'],
props: {
modelValue: {
type: [Array, Object],
default() {
return [];
},
},
url: {
type: [Array, String],
default() {
return [];
},
},
disabled: {
type: Boolean,
default: false,
},
disablePreview: {
type: Boolean,
default: false,
},
delIcon: {
type: Boolean,
default: true,
},
// 自动上传
autoUpload: {
type: Boolean,
default: true,
},
// 最大选择个数 h5只能限制单选或是多选
limit: {
type: [Number, String],
default: 9,
},
// 列表样式 grid | list | list-card
mode: {
type: String,
default: 'grid',
},
// 选择文件类型 image/video/all
fileMediatype: {
type: String,
default: 'image',
},
// 文件类型筛选
fileExtname: {
type: [Array, String],
default() {
return [];
},
},
title: {
type: String,
default: '',
},
listStyles: {
type: Object,
default() {
return {
// 是否显示边框
border: true,
// 是否显示分隔线
dividline: true,
// 线条样式
borderStyle: {},
};
},
},
imageStyles: {
type: Object,
default() {
return {
width: 'auto',
height: 'auto',
};
},
},
readonly: {
type: Boolean,
default: false,
},
sizeType: {
type: Array,
default() {
return ['original', 'compressed'];
},
},
driver: {
type: String,
default: 'local', // local=本地 | oss | unicloud
},
subtitle: {
type: String,
default: '',
},
},
data() {
return {
files: [],
localValue: [],
imgsrc: sheep.$url.static('/static/img/shop/upload-camera.png'),
};
},
watch: {
modelValue: {
handler(newVal, oldVal) {
this.setValue(newVal, oldVal);
},
immediate: true,
},
},
computed: {
returnType() {
if (this.limit > 1) {
return 'array';
}
return 'object';
},
filesList() {
let files = [];
this.files.forEach((v) => {
files.push(v);
});
return files;
},
showType() {
if (this.fileMediatype === 'image') {
return this.mode;
}
return 'list';
},
limitLength() {
if (this.returnType === 'object') {
return 1;
}
if (!this.limit) {
return 1;
}
if (this.limit >= 9) {
return 9;
}
return this.limit;
},
},
created() {
if (this.driver === 'local') {
uniCloud.chooseAndUploadFile = chooseAndUploadFile;
}
this.form = this.getForm('uniForms');
this.formItem = this.getForm('uniFormsItem');
if (this.form && this.formItem) {
if (this.formItem.name) {
this.rename = this.formItem.name;
this.form.inputChildrens.push(this);
}
}
},
methods: {
/**
* 公开用户使用,清空文件
* @param {Object} index
*/
clearFiles(index) {
if (index !== 0 && !index) {
this.files = [];
this.$nextTick(() => {
this.setEmit();
});
} else {
this.files.splice(index, 1);
}
this.$nextTick(() => {
this.setEmit();
});
},
/**
* 公开用户使用,继续上传
*/
upload() {
let files = [];
this.files.forEach((v, index) => {
if (v.status === 'ready' || v.status === 'error') {
files.push(Object.assign({}, v));
}
});
return this.uploadFiles(files);
},
async setValue(newVal, oldVal) {
const newData = async (v) => {
const reg = /cloud:\/\/([\w.]+\/?)\S*/;
let url = '';
if (v.fileID) {
url = v.fileID;
} else {
url = v.url;
}
if (reg.test(url)) {
v.fileID = url;
v.url = await this.getTempFileURL(url);
}
if (v.url) v.path = v.url;
return v;
};
if (this.returnType === 'object') {
if (newVal) {
await newData(newVal);
} else {
newVal = {};
}
} else {
if (!newVal) newVal = [];
for (let i = 0; i < newVal.length; i++) {
let v = newVal[i];
await newData(v);
}
}
this.localValue = newVal;
if (this.form && this.formItem && !this.is_reset) {
this.is_reset = false;
this.formItem.setValue(this.localValue);
}
let filesData = Object.keys(newVal).length > 0 ? newVal : [];
this.files = [].concat(filesData);
},
/**
* 选择文件
*/
choose() {
if (this.disabled) return;
if (
this.files.length >= Number(this.limitLength) &&
this.showType !== 'grid' &&
this.returnType === 'array'
) {
uni.showToast({
title: `您最多选择 ${this.limitLength} 个文件`,
icon: 'none',
});
return;
}
this.chooseFiles();
},
/**
* 选择文件并上传
*/
async chooseFiles() {
const _extname = get_extname(this.fileExtname);
// 获取后缀
await chooseAndUploadFile({
type: this.fileMediatype,
compressed: false,
sizeType: this.sizeType,
// TODO 如果为空video 有问题
extension: _extname.length > 0 ? _extname : undefined,
count: this.limitLength - this.files.length, //默认9
onChooseFile: this.chooseFileCallback,
onUploadProgress: (progressEvent) => {
this.setProgress(progressEvent, progressEvent.index);
},
})
.then((result) => {
this.setSuccessAndError(result);
})
.catch((err) => {
console.log('选择失败', err);
});
},
/**
* 选择文件回调
* @param {Object} res
*/
async chooseFileCallback(res) {
const _extname = get_extname(this.fileExtname);
const is_one =
(Number(this.limitLength) === 1 && this.disablePreview && !this.disabled) ||
this.returnType === 'object';
// 如果这有一个文件 ,需要清空本地缓存数据
if (is_one) {
this.files = [];
}
let { filePaths, files } = get_files_and_is_max(res, _extname);
if (!(_extname && _extname.length > 0)) {
filePaths = res.tempFilePaths;
files = res.tempFiles;
}
let currentData = [];
for (let i = 0; i < files.length; i++) {
if (this.limitLength - this.files.length <= 0) break;
files[i].uuid = Date.now();
let filedata = await get_file_data(files[i], this.fileMediatype);
filedata.progress = 0;
filedata.status = 'ready';
this.files.push(filedata);
currentData.push({
...filedata,
file: files[i],
});
}
this.$emit('select', {
tempFiles: currentData,
tempFilePaths: filePaths,
});
res.tempFiles = files;
// 停止自动上传
if (!this.autoUpload) {
res.tempFiles = [];
}
},
/**
* 批传
* @param {Object} e
*/
uploadFiles(files) {
files = [].concat(files);
return uploadCloudFiles
.call(this, files, 5, (res) => {
this.setProgress(res, res.index, true);
})
.then((result) => {
this.setSuccessAndError(result);
return result;
})
.catch((err) => {
console.log(err);
});
},
/**
* 成功或失败
*/
async setSuccessAndError(res, fn) {
let successData = [];
let errorData = [];
let tempFilePath = [];
let errorTempFilePath = [];
for (let i = 0; i < res.length; i++) {
const item = res[i];
const index = item.uuid ? this.files.findIndex((p) => p.uuid === item.uuid) : item.index;
if (index === -1 || !this.files) break;
if (item.errMsg === 'request:fail') {
this.files[index].url = item.url;
this.files[index].status = 'error';
this.files[index].errMsg = item.errMsg;
// this.files[index].progress = -1
errorData.push(this.files[index]);
errorTempFilePath.push(this.files[index].url);
} else {
this.files[index].errMsg = '';
this.files[index].fileID = item.url;
const reg = /cloud:\/\/([\w.]+\/?)\S*/;
if (reg.test(item.url)) {
this.files[index].url = await this.getTempFileURL(item.url);
} else {
this.files[index].url = item.url;
}
this.files[index].status = 'success';
this.files[index].progress += 1;
successData.push(this.files[index]);
tempFilePath.push(this.files[index].fileID);
}
}
if (successData.length > 0) {
this.setEmit();
// 状态改变返回
this.$emit('success', {
tempFiles: this.backObject(successData),
tempFilePaths: tempFilePath,
});
}
if (errorData.length > 0) {
this.$emit('fail', {
tempFiles: this.backObject(errorData),
tempFilePaths: errorTempFilePath,
});
}
},
/**
* 获取进度
* @param {Object} progressEvent
* @param {Object} index
* @param {Object} type
*/
setProgress(progressEvent, index, type) {
const fileLenth = this.files.length;
const percentNum = (index / fileLenth) * 100;
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
let idx = index;
if (!type) {
idx = this.files.findIndex((p) => p.uuid === progressEvent.tempFile.uuid);
}
if (idx === -1 || !this.files[idx]) return;
// fix by mehaotian 100 就会消失,-1 是为了让进度条消失
this.files[idx].progress = percentCompleted - 1;
// 上传中
this.$emit('progress', {
index: idx,
progress: parseInt(percentCompleted),
tempFile: this.files[idx],
});
},
/**
* 删除文件
* @param {Object} index
*/
delFile(index) {
if (!isEmpty(this.files)) {
this.$emit('delete', {
tempFile: this.files[index],
tempFilePath: this.files[index].url,
});
this.files.splice(index, 1);
} else {
this.$emit('delete', {
tempFilePath: this.url,
});
}
this.$nextTick(() => {
this.setEmit();
});
},
/**
* 获取文件名和后缀
* @param {Object} name
*/
getFileExt(name) {
const last_len = name.lastIndexOf('.');
const len = name.length;
return {
name: name.substring(0, last_len),
ext: name.substring(last_len + 1, len),
};
},
/**
* 处理返回事件
*/
setEmit() {
let data = [];
let updateUrl = [];
if (this.returnType === 'object') {
data = this.backObject(this.files)[0];
this.localValue = data ? data : null;
updateUrl = data ? data.url : '';
} else {
data = this.backObject(this.files);
if (!this.localValue) {
this.localValue = [];
}
this.localValue = [...data];
if (this.localValue.length > 0) {
this.localValue.forEach((item) => {
updateUrl.push(item.url);
});
}
}
this.$emit('update:modelValue', this.localValue);
this.$emit('update:url', updateUrl);
},
/**
* 处理返回参数
* @param {Object} files
*/
backObject(files) {
let newFilesData = [];
files.forEach((v) => {
newFilesData.push({
extname: v.extname,
fileType: v.fileType,
image: v.image,
name: v.name,
path: v.path,
size: v.size,
fileID: v.fileID,
url: v.url,
});
});
return newFilesData;
},
async getTempFileURL(fileList) {
fileList = {
fileList: [].concat(fileList),
};
const urls = await uniCloud.getTempFileURL(fileList);
return urls.fileList[0].tempFileURL || '';
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
},
},
};
</script>
<style lang="scss" scoped>
.uni-file-picker {
/* #ifndef APP-NVUE */
box-sizing: border-box;
overflow: hidden;
/* width: 100%; */
/* #endif */
/* flex: 1; */
position: relative;
}
.uni-file-picker__header {
padding-top: 5px;
padding-bottom: 10px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: space-between;
}
.file-title {
font-size: 14px;
color: #333;
}
.file-count {
font-size: 14px;
color: #999;
}
.is-add {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
}
.add-icon {
width: 57rpx;
height: 49rpx;
}
.file-subtitle {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
width: 140rpx;
height: 36rpx;
z-index: 1;
display: flex;
justify-content: center;
color: #fff;
font-weight: 500;
background: rgba(#000, 0.3);
font-size: 24rpx;
}
</style>

View File

@@ -0,0 +1,335 @@
<template>
<view class="uni-file-picker__files">
<view v-if="!readonly" class="files-button" @click="choose">
<slot></slot>
</view>
<!-- :class="{'is-text-box':showType === 'list'}" -->
<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle">
<!-- ,'is-list-card':showType === 'list-card' -->
<view
class="uni-file-picker__lists-box"
v-for="(item, index) in list"
:key="index"
:class="{
'files-border': index !== 0 && styles.dividline,
}"
:style="index !== 0 && styles.dividline && borderLineStyle"
>
<view class="uni-file-picker__item">
<!-- :class="{'is-text-image':showType === 'list'}" -->
<!-- <view class="files__image is-text-image">
<image class="header-image" :src="item.logo" mode="aspectFit"></image>
</view> -->
<view class="files__name">{{ item.name }}</view>
<view v-if="delIcon && !readonly" class="icon-del-box icon-files" @click="delFile(index)">
<view class="icon-del icon-files"></view>
<view class="icon-del rotate"></view>
</view>
</view>
<view
v-if="(item.progress && item.progress !== 100) || item.progress === 0"
class="file-picker__progress"
>
<progress
class="file-picker__progress-item"
:percent="item.progress === -1 ? 0 : item.progress"
stroke-width="4"
:backgroundColor="item.errMsg ? '#ff5a5f' : '#EBEBEB'"
/>
</view>
<view
v-if="item.status === 'error'"
class="file-picker__mask"
@click.stop="uploadFiles(item, index)"
>
点击重试
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'uploadFile',
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
type: Array,
default() {
return [];
},
},
delIcon: {
type: Boolean,
default: true,
},
limit: {
type: [Number, String],
default: 9,
},
showType: {
type: String,
default: '',
},
listStyles: {
type: Object,
default() {
return {
// 是否显示边框
border: true,
// 是否显示分隔线
dividline: true,
// 线条样式
borderStyle: {},
};
},
},
readonly: {
type: Boolean,
default: false,
},
},
computed: {
list() {
let files = [];
this.filesList.forEach((v) => {
files.push(v);
});
return files;
},
styles() {
let styles = {
border: true,
dividline: true,
'border-style': {},
};
return Object.assign(styles, this.listStyles);
},
borderStyle() {
let { borderStyle, border } = this.styles;
let obj = {};
if (!border) {
obj.border = 'none';
} else {
let width = (borderStyle && borderStyle.width) || 1;
width = this.value2px(width);
let radius = (borderStyle && borderStyle.radius) || 5;
radius = this.value2px(radius);
obj = {
'border-width': width,
'border-style': (borderStyle && borderStyle.style) || 'solid',
'border-color': (borderStyle && borderStyle.color) || '#eee',
'border-radius': radius,
};
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
borderLineStyle() {
let obj = {};
let { borderStyle } = this.styles;
if (borderStyle && borderStyle.color) {
obj['border-color'] = borderStyle.color;
}
if (borderStyle && borderStyle.width) {
let width = (borderStyle && borderStyle.width) || 1;
let style = (borderStyle && borderStyle.style) || 0;
if (typeof width === 'number') {
width += 'px';
} else {
width = width.indexOf('px') ? width : width + 'px';
}
obj['border-width'] = width;
if (typeof style === 'number') {
style += 'px';
} else {
style = style.indexOf('px') ? style : style + 'px';
}
obj['border-top-style'] = style;
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
},
methods: {
uploadFiles(item, index) {
this.$emit('uploadFiles', {
item,
index,
});
},
choose() {
this.$emit('choose');
},
delFile(index) {
this.$emit('delFile', index);
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
} else {
value = value.indexOf('px') !== -1 ? value : value + 'px';
}
return value;
},
},
};
</script>
<style lang="scss">
.uni-file-picker__files {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: flex-start;
}
.files-button {
// border: 1px red solid;
}
.uni-file-picker__lists {
position: relative;
margin-top: 5px;
overflow: hidden;
}
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
color: #fff;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.4);
}
.uni-file-picker__lists-box {
position: relative;
}
.uni-file-picker__item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
padding: 8px 10px;
padding-right: 5px;
padding-left: 10px;
}
.files-border {
border-top: 1px #eee solid;
}
.files__name {
flex: 1;
font-size: 14px;
color: #666;
margin-right: 25px;
/* #ifndef APP-NVUE */
word-break: break-all;
word-wrap: break-word;
/* #endif */
}
.icon-files {
/* #ifndef APP-NVUE */
position: static;
background-color: initial;
/* #endif */
}
// .icon-files .icon-del {
// background-color: #333;
// width: 12px;
// height: 1px;
// }
.is-list-card {
border: 1px #eee solid;
margin-bottom: 5px;
border-radius: 5px;
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
padding: 5px;
}
.files__image {
width: 40px;
height: 40px;
margin-right: 10px;
}
.header-image {
width: 100%;
height: 100%;
}
.is-text-box {
border: 1px #eee solid;
border-radius: 5px;
}
.is-text-image {
width: 25px;
height: 25px;
margin-left: 5px;
}
.rotate {
position: absolute;
transform: rotate(90deg);
}
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
margin: auto 0;
/* #endif */
align-items: center;
justify-content: center;
position: absolute;
top: 0px;
bottom: 0;
right: 5px;
height: 26px;
width: 26px;
// border-radius: 50%;
// background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
transform: rotate(-45deg);
}
.icon-del {
width: 15px;
height: 1px;
background-color: #333;
// border-radius: 1px;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.uni-file-picker__files {
max-width: 375px;
}
}
/* #endif */
</style>

View File

@@ -0,0 +1,306 @@
<template>
<view class="uni-file-picker__container">
<view class="file-picker__box" v-for="(url, index) in list" :key="index" :style="boxStyle">
<view class="file-picker__box-content" :style="borderStyle">
<image
class="file-image"
:src="getImageUrl(url)"
mode="aspectFill"
@click.stop="previewImage(url, index)"
></image>
<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
<view class="icon-del"></view>
<view class="icon-del rotate"></view>
</view>
<!-- <view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item, index)">
点击重试
</view> -->
</view>
</view>
<view v-if="list.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
<slot>
<view class="icon-add"></view>
<view class="icon-add rotate"></view>
</slot>
</view>
</view>
</view>
</template>
<script>
import sheep from '@/sheep';
export default {
name: 'uploadImage',
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
type: [Array, String],
default() {
return [];
},
},
disabled: {
type: Boolean,
default: false,
},
disablePreview: {
type: Boolean,
default: false,
},
limit: {
type: [Number, String],
default: 9,
},
imageStyles: {
type: Object,
default() {
return {
width: 'auto',
height: 'auto',
border: {},
};
},
},
delIcon: {
type: Boolean,
default: true,
},
readonly: {
type: Boolean,
default: false,
},
},
computed: {
list() {
if (typeof this.filesList === 'string') {
if (this.filesList) {
return [this.filesList];
} else {
return [];
}
}
return this.filesList;
},
styles() {
let styles = {
width: 'auto',
height: 'auto',
border: {},
};
return Object.assign(styles, this.imageStyles);
},
boxStyle() {
const { width = 'auto', height = 'auto' } = this.styles;
let obj = {};
if (height === 'auto') {
if (width !== 'auto') {
obj.height = this.value2px(width);
obj['padding-top'] = 0;
} else {
obj.height = 0;
}
} else {
obj.height = this.value2px(height);
obj['padding-top'] = 0;
}
if (width === 'auto') {
if (height !== 'auto') {
obj.width = this.value2px(height);
} else {
obj.width = '33.3%';
}
} else {
obj.width = this.value2px(width);
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
borderStyle() {
let { border } = this.styles;
let obj = {};
const widthDefaultValue = 1;
const radiusDefaultValue = 3;
if (typeof border === 'boolean') {
obj.border = border ? '1px #eee solid' : 'none';
} else {
let width = (border && border.width) || widthDefaultValue;
width = this.value2px(width);
let radius = (border && border.radius) || radiusDefaultValue;
radius = this.value2px(radius);
obj = {
'border-width': width,
'border-style': (border && border.style) || 'solid',
'border-color': (border && border.color) || '#eee',
'border-radius': radius,
};
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
},
methods: {
getImageUrl(url) {
if ('blob:http:' === url.substr(0, 10)) {
return url;
} else {
return sheep.$url.cdn(url);
}
},
uploadFiles(item, index) {
this.$emit('uploadFiles', item);
},
choose() {
this.$emit('choose');
},
delFile(index) {
this.$emit('delFile', index);
},
previewImage(img, index) {
let urls = [];
if (Number(this.limit) === 1 && this.disablePreview && !this.disabled) {
this.$emit('choose');
}
if (this.disablePreview) return;
this.list.forEach((i) => {
urls.push(this.getImageUrl(i));
});
uni.previewImage({
urls: urls,
current: index,
});
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
} else {
if (value.indexOf('%') === -1) {
value = value.indexOf('px') !== -1 ? value : value + 'px';
}
}
return value;
},
},
};
</script>
<style lang="scss">
.uni-file-picker__container {
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
/* #endif */
flex-wrap: wrap;
margin: -5px;
}
.file-picker__box {
position: relative;
// flex: 0 0 33.3%;
width: 33.3%;
height: 0;
padding-top: 33.33%;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.file-picker__box-content {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: 5px;
border: 1px #eee solid;
border-radius: 5px;
overflow: hidden;
}
.file-picker__progress {
position: absolute;
bottom: 0;
left: 0;
right: 0;
/* border: 1px red solid; */
z-index: 2;
}
.file-picker__progress-item {
width: 100%;
}
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
color: #fff;
font-size: 12px;
background-color: rgba(0, 0, 0, 0.4);
}
.file-image {
width: 100%;
height: 100%;
}
.is-add {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
}
.icon-add {
width: 50px;
height: 5px;
background-color: #f1f1f1;
border-radius: 2px;
}
.rotate {
position: absolute;
transform: rotate(90deg);
}
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
position: absolute;
top: 3px;
right: 3px;
height: 26px;
width: 26px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
transform: rotate(-45deg);
}
.icon-del {
width: 15px;
height: 2px;
background-color: #fff;
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,109 @@
/**
* 获取文件名和后缀
* @param {String} name
*/
export const get_file_ext = (name) => {
const last_len = name.lastIndexOf('.');
const len = name.length;
return {
name: name.substring(0, last_len),
ext: name.substring(last_len + 1, len),
};
};
/**
* 获取扩展名
* @param {Array} fileExtname
*/
export const get_extname = (fileExtname) => {
if (!Array.isArray(fileExtname)) {
let extname = fileExtname.replace(/([\[\]])/g, '');
return extname.split(',');
} else {
return fileExtname;
}
};
/**
* 获取文件和检测是否可选
*/
export const get_files_and_is_max = (res, _extname) => {
let filePaths = [];
let files = [];
if (!_extname || _extname.length === 0) {
return {
filePaths,
files,
};
}
res.tempFiles.forEach((v) => {
let fileFullName = get_file_ext(v.name);
const extname = fileFullName.ext.toLowerCase();
if (_extname.indexOf(extname) !== -1) {
files.push(v);
filePaths.push(v.path);
}
});
if (files.length !== res.tempFiles.length) {
uni.showToast({
title: `当前选择了${res.tempFiles.length}个文件 ${
res.tempFiles.length - files.length
} 个文件格式不正确`,
icon: 'none',
duration: 5000,
});
}
return {
filePaths,
files,
};
};
/**
* 获取图片信息
* @param {Object} filepath
*/
export const get_file_info = (filepath) => {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: filepath,
success(res) {
resolve(res);
},
fail(err) {
reject(err);
},
});
});
};
/**
* 获取封装数据
*/
export const get_file_data = async (files, type = 'image') => {
// 最终需要上传数据库的数据
let fileFullName = get_file_ext(files.name);
const extname = fileFullName.ext.toLowerCase();
let filedata = {
name: files.name,
uuid: files.uuid,
extname: extname || '',
cloudPath: files.cloudPath,
fileType: files.fileType,
url: files.url || files.path,
size: files.size, //单位是字节
image: {},
path: files.path,
video: {},
};
if (type === 'image') {
const imageinfo = await get_file_info(files.path);
delete filedata.video;
filedata.image.width = imageinfo.width;
filedata.image.height = imageinfo.height;
filedata.image.location = imageinfo.path;
} else {
delete filedata.image;
}
return filedata;
};

View File

@@ -0,0 +1,569 @@
<template>
<view>
<view v-if="visible" class="s-verify-mask" @touchmove.stop.prevent>
<view class="s-verify-wrapper" @tap.stop>
<view class="s-verify-header">
<text class="s-verify-title">安全验证</text>
<view class="s-verify-close" @tap="handleClose">×</view>
</view>
<view class="s-verify-body">
<view class="s-verify-image" :style="imageStyle">
<image
v-if="backgroundImage"
class="s-verify-image-bg"
:src="backgroundImage"
mode="aspectFill"
/>
<image
v-if="showBlock"
class="s-verify-image-block"
:style="blockStyle"
:src="blockImage"
mode="widthFix"
/>
<view class="s-verify-refresh" @tap="refreshCaptcha">
<text></text>
</view>
<view v-if="loading" class="s-verify-loading">加载中...</view>
</view>
<view v-if="tipMessage" :class="['s-verify-tip', tipClass]">
{{ tipMessage }}
</view>
<view
class="s-verify-slider"
ref="sliderRef"
@touchstart.stop.prevent="onDragStart"
@touchmove.stop.prevent="onDragMove"
@touchend.stop.prevent="onDragEnd"
@mousedown.stop="onMouseDown"
>
<view class="s-verify-slider-track" />
<view class="s-verify-slider-fill" :style="sliderFillStyle" />
<view
class="s-verify-slider-handle"
:class="{
's-verify-slider-success': success,
's-verify-slider-fail': failAnimate
}"
:style="sliderHandleStyle"
>
<text class="s-verify-handle-icon">{{ success ? '✔' : '' }}</text>
</view>
<text class="s-verify-slider-text">{{ sliderHint }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue';
import AuthUtil from '@/sheep/api/system/auth';
import { aesEncrypt } from '@/sheep/components/s-verify/utils/aes';
const props = defineProps({
captchaType: {
type: String,
default: 'blockPuzzle',
},
mode: {
type: String,
default: 'pop',
},
});
const emits = defineEmits(['success', 'error']);
const visible = ref(false);
const loading = ref(false);
const success = ref(false);
const failAnimate = ref(false);
const verifying = ref(false);
const tipMessage = ref('');
const sliderRef = ref(null);
const sliderWidth = 310;
const sliderHeight = 40;
const blockWidth = Math.floor((sliderWidth * 47) / 310);
const imageHeight = 155;
const dragState = reactive({
startClientX: 0,
baseOffset: 0,
isDragging: false,
});
const captchaState = reactive({
originalImageBase64: '',
jigsawImageBase64: '',
token: '',
secretKey: '',
});
const handleOffset = ref(0);
const maxOffset = computed(() => Math.max(sliderWidth - blockWidth, 0));
const showBlock = computed(() => props.captchaType === 'blockPuzzle' && blockImage.value);
const backgroundImage = computed(() =>
captchaState.originalImageBase64
? `data:image/png;base64,${captchaState.originalImageBase64}`
: ''
);
const blockImage = computed(() =>
captchaState.jigsawImageBase64
? `data:image/png;base64,${captchaState.jigsawImageBase64}`
: ''
);
const imageStyle = computed(() => ({
width: `${sliderWidth}px`,
height: `${imageHeight}px`,
}));
const blockStyle = computed(() => ({
width: `${blockWidth}px`,
height: `${imageHeight}px`,
transform: `translateX(${handleOffset.value}px)`,
}));
const sliderHandleStyle = computed(() => ({
width: `${blockWidth}px`,
height: `${sliderHeight}px`,
transform: `translateX(${handleOffset.value}px)`,
}));
const sliderFillStyle = computed(() => ({
width: `${handleOffset.value > 0 ? Math.min(handleOffset.value + blockWidth, sliderWidth) : 0}px`,
}));
const sliderHint = computed(() => {
if (success.value) {
return '验证通过';
}
if (loading.value) {
return '验证码加载中';
}
return '请按住滑块,拖动完成拼图';
});
const tipClass = computed(() => {
if (success.value) return 's-verify-tip-success';
if (failAnimate.value) return 's-verify-tip-error';
return '';
});
let closeTimer = null;
let mouseMoveListener = null;
let mouseUpListener = null;
function show() {
if (visible.value) return;
visible.value = true;
resetState();
nextTick(() => {
loadCaptcha();
});
}
function handleClose() {
visible.value = false;
cleanupMouseListeners();
clearCloseTimer();
}
function refreshCaptcha() {
if (loading.value) return;
resetSlider();
loadCaptcha();
}
function resetSlider() {
handleOffset.value = 0;
dragState.baseOffset = 0;
dragState.startClientX = 0;
dragState.isDragging = false;
success.value = false;
failAnimate.value = false;
tipMessage.value = '';
verifying.value = false;
}
function resetState() {
captchaState.originalImageBase64 = '';
captchaState.jigsawImageBase64 = '';
captchaState.token = '';
captchaState.secretKey = '';
resetSlider();
}
async function loadCaptcha() {
loading.value = true;
try {
const res = await AuthUtil.getCaptchaCode({ captchaType: props.captchaType });
if (res && (res.repCode === '0000' || res.code === 0)) {
const data = res.repData || res.data || {};
captchaState.originalImageBase64 = data.originalImageBase64 || '';
captchaState.jigsawImageBase64 = data.jigsawImageBase64 || '';
captchaState.token = data.token || '';
captchaState.secretKey = data.secretKey || '';
} else {
tipMessage.value = res?.repMsg || res?.msg || '验证码获取失败,请重试';
emits('error', res);
}
} catch (error) {
console.error('captcha load error', error);
tipMessage.value = '验证码加载异常,请稍后再试';
emits('error', error);
} finally {
loading.value = false;
}
}
function onDragStart(event) {
if (loading.value || success.value) return;
dragState.isDragging = true;
dragState.startClientX = getClientX(event);
dragState.baseOffset = handleOffset.value;
failAnimate.value = false;
tipMessage.value = '';
}
function onDragMove(event) {
if (!dragState.isDragging) return;
const currentX = getClientX(event);
const delta = currentX - dragState.startClientX;
let next = dragState.baseOffset + delta;
if (next < 0) next = 0;
if (next > maxOffset.value) next = maxOffset.value;
handleOffset.value = next;
}
function onDragEnd() {
if (!dragState.isDragging) return;
dragState.isDragging = false;
submitVerification();
}
function onMouseDown(event) {
if (event?.button !== 0) return;
if (typeof window !== 'undefined') {
mouseMoveListener = (ev) => {
onDragMove(ev);
ev.preventDefault();
};
mouseUpListener = (ev) => {
cleanupMouseListeners();
onDragEnd(ev);
};
window.addEventListener('mousemove', mouseMoveListener);
window.addEventListener('mouseup', mouseUpListener);
}
onDragStart(event);
}
function cleanupMouseListeners() {
if (typeof window === 'undefined') return;
if (mouseMoveListener) {
window.removeEventListener('mousemove', mouseMoveListener);
mouseMoveListener = null;
}
if (mouseUpListener) {
window.removeEventListener('mouseup', mouseUpListener);
mouseUpListener = null;
}
}
function clearCloseTimer() {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
}
async function submitVerification() {
if (verifying.value) return;
verifying.value = true;
failAnimate.value = false;
const point = {
x: Math.round(handleOffset.value),
y: 5.0,
};
try {
const payload = captchaState.secretKey
? aesEncrypt(JSON.stringify(point), captchaState.secretKey)
: JSON.stringify(point);
const res = await AuthUtil.verifyCaptcha({
captchaType: props.captchaType,
token: captchaState.token,
pointJson: payload,
});
if (res && (res.repCode === '0000' || res.code === 0)) {
success.value = true;
tipMessage.value = '验证成功';
const captchaVerification = captchaState.secretKey
? aesEncrypt(
`${captchaState.token}---${JSON.stringify(point)}`,
captchaState.secretKey,
)
: `${captchaState.token}---${JSON.stringify(point)}`;
emits('success', {
captchaVerification,
token: captchaState.token,
point,
});
clearCloseTimer();
closeTimer = setTimeout(() => {
handleClose();
}, 800);
} else {
handleFail(res?.repMsg || res?.msg || '验证失败,请重试');
}
} catch (error) {
console.error('captcha verify error', error);
handleFail('网络异常,请重试');
} finally {
verifying.value = false;
}
}
function handleFail(message) {
failAnimate.value = true;
tipMessage.value = message;
emits('error', message);
setTimeout(() => {
failAnimate.value = false;
}, 400);
setTimeout(() => {
refreshCaptcha();
}, 500);
}
function getClientX(event) {
if (event?.touches && event.touches.length) {
return event.touches[0].clientX;
}
if (event?.changedTouches && event.changedTouches.length) {
return event.changedTouches[0].clientX;
}
return event?.clientX || 0;
}
onBeforeUnmount(() => {
cleanupMouseListeners();
clearCloseTimer();
});
defineExpose({
show,
close: handleClose,
refresh: refreshCaptcha,
});
</script>
<style lang="scss" scoped>
.s-verify-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.s-verify-wrapper {
width: 340px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.s-verify-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f2f3f5;
}
.s-verify-title {
font-size: 16px;
font-weight: 600;
color: #1f2d3d;
}
.s-verify-close {
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 50%;
color: #999999;
font-size: 18px;
}
.s-verify-body {
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.s-verify-image {
position: relative;
border-radius: 12px;
overflow: hidden;
background: #f5f5f5;
}
.s-verify-image-bg {
width: 100%;
height: 100%;
display: block;
}
.s-verify-image-block {
position: absolute;
top: 0;
display: block;
}
.s-verify-refresh {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.45);
color: #ffffff;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.s-verify-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.65);
font-size: 14px;
color: #666666;
}
.s-verify-tip {
width: 100%;
text-align: center;
font-size: 14px;
padding: 6px 0;
border-radius: 6px;
}
.s-verify-tip-success {
color: #52c41a;
background: rgba(82, 196, 26, 0.12);
}
.s-verify-tip-error {
color: #ff4d4f;
background: rgba(255, 77, 79, 0.12);
}
.s-verify-slider {
position: relative;
width: 310px;
height: 40px;
border-radius: 20px;
background: #f2f3f5;
overflow: hidden;
}
.s-verify-slider-track {
position: absolute;
inset: 0;
border-radius: 20px;
background: #f2f3f5;
}
.s-verify-slider-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: var(--ui-BG-Main, #409eff);
border-radius: 20px;
transition: width 0.05s linear;
}
.s-verify-slider-handle {
position: absolute;
top: 0;
background: #ffffff;
border-radius: 20px;
box-shadow: 0 6px 14px rgba(25, 87, 170, 0.25);
display: flex;
align-items: center;
justify-content: center;
color: #1f2d3d;
transition: transform 0.05s linear;
}
.s-verify-slider-success {
background: #52c41a;
color: #ffffff;
}
.s-verify-slider-fail {
animation: s-verify-shake 0.3s;
}
.s-verify-handle-icon {
font-size: 22px;
font-weight: 600;
}
.s-verify-slider-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #8c8c8c;
font-size: 14px;
}
@keyframes s-verify-shake {
0% {
transform: translateX(-4px);
}
25% {
transform: translateX(4px);
}
50% {
transform: translateX(-2px);
}
75% {
transform: translateX(2px);
}
100% {
transform: translateX(0);
}
}
</style>

View File

@@ -0,0 +1,17 @@
import CryptoJS from 'crypto-js';
/**
* 使用 AES-ECB 模式 + PKCS7 填充对字符串进行加密
* @param {string} word - 需要加密的明文
* @param {string} [keyWord='XwKsGlMcdPMEhR1B'] - 加密密钥,默认为 aj-captcha 默认密钥
* @returns {string} - 加密后的密文
*/
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
const key = CryptoJS.enc.Utf8.parse(keyWord);
const srcs = CryptoJS.enc.Utf8.parse(word);
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}

View File

@@ -0,0 +1,227 @@
/**
* 验证码组件工具函数
*/
/**
* 生成随机整数
* @param {number} min 最小值
* @param {number} max 最大值
* @returns {number} 随机整数
*/
export function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 生成随机颜色
* @returns {string} 十六进制颜色值
*/
export function randomColor() {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
return colors[Math.floor(Math.random() * colors.length)];
}
/**
* 生成随机字符串
* @param {number} length 字符串长度
* @param {string} chars 字符集
* @returns {string} 随机字符串
*/
export function randomString(length = 4, chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678') {
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 计算两点距离
* @param {number} x1 点1 x坐标
* @param {number} y1 点1 y坐标
* @param {number} x2 点2 x坐标
* @param {number} y2 点2 y坐标
* @returns {number} 距离
*/
export function getDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
/**
* 判断点是否在矩形区域内
* @param {number} x 点x坐标
* @param {number} y 点y坐标
* @param {Object} rect 矩形对象 {x, y, width, height}
* @returns {boolean} 是否在区域内
*/
export function isPointInRect(x, y, rect) {
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
/**
* 生成验证码图片拼图
* @param {CanvasRenderingContext2D} ctx Canvas上下文
* @param {number} x x坐标
* @param {number} y y坐标
* @param {number} r 圆角半径
* @param {number} PI 圆周率
*/
export function drawPath(ctx, x, y, r, PI) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.arc(x + r / 2, y - r + 2, r, 0, 2 * PI);
ctx.lineTo(x + r, y);
ctx.arc(x + r + r / 2, y + r / 2, r, 1.5 * PI, 0.5 * PI);
ctx.lineTo(x + r, y + r);
ctx.lineTo(x, y + r);
ctx.arc(x + r / 2, y + r + r / 2, r, PI, 0);
ctx.closePath();
}
/**
* 获取图片像素数据
* @param {string} imgSrc 图片源
* @returns {Promise<ImageData>} 图片像素数据
*/
export function getImageData(imgSrc) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
resolve(ctx.getImageData(0, 0, canvas.width, canvas.height));
};
img.onerror = reject;
img.crossOrigin = 'anonymous';
img.src = imgSrc;
});
}
/**
* 创建干扰线
* @param {CanvasRenderingContext2D} ctx Canvas上下文
* @param {number} width 画布宽度
* @param {number} height 画布高度
* @param {number} lineCount 干扰线数量
*/
export function drawInterferenceLines(ctx, width, height, lineCount = 8) {
for (let i = 0; i < lineCount; i++) {
ctx.strokeStyle = randomColor();
ctx.lineWidth = randomInt(1, 3);
ctx.beginPath();
ctx.moveTo(randomInt(0, width), randomInt(0, height));
ctx.lineTo(randomInt(0, width), randomInt(0, height));
ctx.stroke();
}
}
/**
* 创建干扰点
* @param {CanvasRenderingContext2D} ctx Canvas上下文
* @param {number} width 画布宽度
* @param {number} height 画布高度
* @param {number} pointCount 干扰点数量
*/
export function drawInterferencePoints(ctx, width, height, pointCount = 100) {
for (let i = 0; i < pointCount; i++) {
ctx.fillStyle = randomColor();
ctx.beginPath();
ctx.arc(randomInt(0, width), randomInt(0, height), randomInt(1, 2), 0, 2 * Math.PI);
ctx.fill();
}
}
/**
* 节流函数
* @param {Function} func 要执行的函数
* @param {number} delay 延迟时间
* @returns {Function} 节流后的函数
*/
export function throttle(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
/**
* 防抖函数
* @param {Function} func 要执行的函数
* @param {number} delay 延迟时间
* @returns {Function} 防抖后的函数
*/
export function debounce(func, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
/**
* 重置画布
* @param {CanvasRenderingContext2D} ctx Canvas上下文
* @param {number} width 画布宽度
* @param {number} height 画布高度
*/
export function resetCanvas(ctx, width, height) {
ctx.clearRect(0, 0, width, height);
}
/**
* 获取元素的绝对位置
* @param {Element} element DOM元素
* @returns {Object} {left, top}
*/
export function getElementPosition(element) {
let left = 0;
let top = 0;
while (element) {
left += element.offsetLeft;
top += element.offsetTop;
element = element.offsetParent;
}
return { left, top };
}
/**
* 移动端触摸事件坐标获取
* @param {Event} e 事件对象
* @returns {Object} {x, y}
*/
export function getTouchPosition(e) {
const touch = e.touches && e.touches.length > 0 ? e.touches[0] : e.changedTouches[0];
return {
x: touch.clientX,
y: touch.clientY
};
}
/**
* 获取鼠标或触摸位置
* @param {Event} e 事件对象
* @returns {Object} {x, y}
*/
export function getEventPosition(e) {
if (e.touches) {
return getTouchPosition(e);
}
return {
x: e.clientX,
y: e.clientY
};
}
export { aesEncrypt } from './aes';

3
sheep/config/captcha.js Normal file
View File

@@ -0,0 +1,3 @@
export default {
captchaEnable: true, // 是否开启验证码
}

31
sheep/config/index.js Normal file
View File

@@ -0,0 +1,31 @@
import packageInfo from '@/package.json';
const { version } = packageInfo;
// 开发环境配置
export let baseUrl;
if (process.env.NODE_ENV === 'development') {
baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL;
} else {
baseUrl = import.meta.env.SHOPRO_BASE_URL;
}
if (typeof baseUrl === 'undefined') {
console.error('请检查.env配置文件是否存在');
} else {
console.log(`[移动端 ${version}] https://doc.iocoder.cn`);
}
export const apiPath = import.meta.env.SHOPRO_API_PATH;
export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
export const tenantId = import.meta.env.SHOPRO_TENANT_ID;
export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH;
export const h5Url = import.meta.env.SHOPRO_H5_URL;
export default {
baseUrl,
apiPath,
staticUrl,
tenantId,
websocketPath,
h5Url,
};

35
sheep/config/theme.js Normal file
View File

@@ -0,0 +1,35 @@
// 主题配置文件
export const themeConfig = {
// 主题色配置 - 统一使用 #0055A2
primary: {
main: '#0055A2',
light: '#337AB7',
dark: '#003F73',
gradient: 'rgba(0, 85, 162, 0.6)', // 渐变结束色
},
// 渐变配置
gradients: {
// 水平渐变 (90度)
horizontal: 'linear-gradient(90deg, #0055A2, rgba(0, 85, 162, 0.6))',
// 垂直渐变 (180度)
vertical: 'linear-gradient(180deg, #0055A2 0%, rgba(0, 85, 162, 0.6) 100%)',
// 对角渐变
diagonal: 'linear-gradient(135deg, #0055A2, rgba(0, 85, 162, 0.6))',
},
// CSS 变量映射
getCSSVars() {
return {
'--theme-primary': this.primary.main,
'--theme-primary-light': this.primary.light,
'--theme-primary-dark': this.primary.dark,
'--theme-primary-gradient': this.primary.gradient,
'--gradient-horizontal-primary': this.gradients.horizontal,
'--gradient-vertical-primary': this.gradients.vertical,
'--gradient-diagonal-primary': this.gradients.diagonal,
};
}
};
export default themeConfig;

20
sheep/config/zIndex.js Normal file
View File

@@ -0,0 +1,20 @@
// uniapp在H5中各API的z-index值如下
/**
* actionsheet: 999
* modal: 999
* navigate: 998
* tabbar: 998
* toast: 999
*/
export default {
toast: 10090,
noNetwork: 10080,
popup: 10075, // popup包含popupactionsheetkeyboardpicker的值
mask: 10070,
navbar: 980,
topTips: 975,
sticky: 970,
indexListSticky: 965,
popover: 960,
};

46
sheep/helper/const.js Normal file
View File

@@ -0,0 +1,46 @@
// ========== COMMON - 公共模块 ==========
/**
* 与后端Terminal枚举一一对应
*/
export const TerminalEnum = {
UNKNOWN: 0, // 未知, 目的:在无法解析到 terminal 时,使用它
WECHAT_MINI_PROGRAM: 10, //微信小程序
WECHAT_WAP: 11, // 微信公众号
H5: 20, // H5 网页
APP: 31, // 手机 App
};
/**
* 分享页面枚举
*/
export const SharePageEnum = {
HOME: {
value: 'home',
page: '/pages/index/index'
},
GOODS: {
value: 'goods',
page: '/pages/index/index'
}
};
/**
* 将 uni-app 提供的平台转换为后端所需的 terminal值
*
* @return 终端
*/
export const getTerminal = () => {
const platformType = uni.getAppBaseInfo().uniPlatform;
// 与后端terminal枚举一一对应
switch (platformType) {
case 'app':
return TerminalEnum.APP;
case 'web':
return TerminalEnum.H5;
case 'mp-weixin':
return TerminalEnum.WECHAT_MINI_PROGRAM;
default:
return TerminalEnum.UNKNOWN;
}
};

168
sheep/helper/digit.js Normal file
View File

@@ -0,0 +1,168 @@
let _boundaryCheckingState = true; // 是否进行越界检查的全局开关
/**
* 把错误的数据转正
* @private
* @example strip(0.09999999999999998)=0.1
*/
function strip(num, precision = 15) {
return +parseFloat(Number(num).toPrecision(precision));
}
/**
* Return digits length of a number
* @private
* @param {*number} num Input number
*/
function digitLength(num) {
// Get digit length of e
const eSplit = num.toString().split(/[eE]/);
const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0);
return len > 0 ? len : 0;
}
/**
* 把小数转成整数,如果是小数则放大成整数
* @private
* @param {*number} num 输入数
*/
function float2Fixed(num) {
if (num.toString().indexOf('e') === -1) {
return Number(num.toString().replace('.', ''));
}
const dLen = digitLength(num);
return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num);
}
/**
* 检测数字是否越界,如果越界给出提示
* @private
* @param {*number} num 输入数
*/
function checkBoundary(num) {
if (_boundaryCheckingState) {
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
console.warn(`${num} 超出了精度限制,结果可能不正确`);
}
}
}
/**
* 把递归操作扁平迭代化
* @param {number[]} arr 要操作的数字数组
* @param {function} operation 迭代操作
* @private
*/
function iteratorOperation(arr, operation) {
const [num1, num2, ...others] = arr;
let res = operation(num1, num2);
others.forEach((num) => {
res = operation(res, num);
});
return res;
}
/**
* 高精度乘法
* @export
*/
export function times(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, times);
}
const [num1, num2] = nums;
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
const baseNum = digitLength(num1) + digitLength(num2);
const leftValue = num1Changed * num2Changed;
checkBoundary(leftValue);
return leftValue / Math.pow(10, baseNum);
}
/**
* 高精度加法
* @export
*/
export function plus(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, plus);
}
const [num1, num2] = nums;
// 取最大的小数位
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
// 把小数都转为整数然后再计算
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
}
/**
* 高精度减法
* @export
*/
export function minus(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, minus);
}
const [num1, num2] = nums;
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
}
/**
* 高精度除法
* @export
*/
export function divide(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, divide);
}
const [num1, num2] = nums;
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
checkBoundary(num1Changed);
checkBoundary(num2Changed);
// 重要这里必须用strip进行修正
return times(
num1Changed / num2Changed,
strip(Math.pow(10, digitLength(num2) - digitLength(num1))),
);
}
/**
* 四舍五入
* @export
*/
export function round(num, ratio) {
const base = Math.pow(10, ratio);
let result = divide(Math.round(Math.abs(times(num, base))), base);
if (num < 0 && result !== 0) {
result = times(result, -1);
}
// 位数不足则补0
return result;
}
/**
* 是否进行边界检查,默认开启
* @param flag 标记开关true 为开启false 为关闭,默认为 true
* @export
*/
export function enableBoundaryChecking(flag = true) {
_boundaryCheckingState = flag;
}
export default {
times,
plus,
minus,
divide,
round,
enableBoundaryChecking,
};

703
sheep/helper/index.js Normal file
View File

@@ -0,0 +1,703 @@
import test from './test.js';
import { round } from './digit.js';
/**
* @description 如果value小于min取min如果value大于max取max
* @param {number} min
* @param {number} max
* @param {number} value
*/
function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)));
}
/**
* @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx",取出其数值部分,如果是"xxxrpx"还需要用过uni.upx2px进行转换
* @param {number|string} value 用户传递值的px值
* @param {boolean} unit
* @returns {number|string}
*/
export function getPx(value, unit = false) {
if (test.number(value)) {
return unit ? `${value}px` : Number(value);
}
// 如果带有rpx先取出其数值部分再转为px值
if (/(rpx|upx)$/.test(value)) {
return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value)));
}
return unit ? `${parseInt(value)}px` : parseInt(value);
}
/**
* @description 进行延时,以达到可以简写代码的目的
* @param {number} value 堵塞时间 单位ms 毫秒
* @returns {Promise} 返回promise
*/
export function sleep(value = 30) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, value);
});
}
/**
* @description 运行期判断平台
* @returns {string} 返回所在平台(小写)
* @link 运行期判断平台 https://uniapp.dcloud.io/frame?id=判断平台
*/
export function os() {
return uni.getDeviceInfo().platform.toLowerCase();
}
/**
* @description 取一个区间数
* @param {Number} min 最小值
* @param {Number} max 最大值
*/
function random(min, max) {
if (min >= 0 && max > 0 && max >= min) {
const gab = max - min + 1;
return Math.floor(Math.random() * gab + min);
}
return 0;
}
/**
* @param {Number} len uuid的长度
* @param {Boolean} firstU 将返回的首字母置为"u"
* @param {Nubmer} radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制
*/
export function guid(len = 32, firstU = true, radix = null) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid = [];
radix = radix || chars.length;
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
let r;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
if (firstU) {
uuid.shift();
return `u${uuid.join('')}`;
}
return uuid.join('');
}
/**
* @description 获取父组件的参数因为支付宝小程序不支持provide/inject的写法
this.$parent在非H5中可以准确获取到父组件但是在H5中需要多次this.$parent.$parent.xxx
这里默认值等于undefined有它的含义因为最顶层元素(组件)的$parent就是undefined意味着不传name
值(默认为undefined),就是查找最顶层的$parent
* @param {string|undefined} name 父组件的参数名
*/
export function $parent(name = undefined) {
let parent = this.$parent;
// 通过while历遍这里主要是为了H5需要多层解析的问题
while (parent) {
// 父组件
if (parent.$options && parent.$options.name !== name) {
// 如果组件的name不相等继续上一级寻找
parent = parent.$parent;
} else {
return parent;
}
}
return false;
}
/**
* @description 样式转换
* 对象转字符串,或者字符串转对象
* @param {object | string} customStyle 需要转换的目标
* @param {String} target 转换的目的object-转为对象string-转为字符串
* @returns {object|string}
*/
export function addStyle(customStyle, target = 'object') {
// 字符串转字符串,对象转对象情形,直接返回
if (
test.empty(customStyle) ||
(typeof customStyle === 'object' && target === 'object') ||
(target === 'string' && typeof customStyle === 'string')
) {
return customStyle;
}
// 字符串转对象
if (target === 'object') {
// 去除字符串样式中的两端空格(中间的空格不能去掉比如padding: 20px 0如果去掉了就错了),空格是无用的
customStyle = trim(customStyle);
// 根据";"将字符串转为数组形式
const styleArray = customStyle.split(';');
const style = {};
// 历遍数组,拼接成对象
for (let i = 0; i < styleArray.length; i++) {
// 'font-size:20px;color:red;',如此最后字符串有";"的话会导致styleArray最后一个元素为空字符串这里需要过滤
if (styleArray[i]) {
const item = styleArray[i].split(':');
style[trim(item[0])] = trim(item[1]);
}
}
return style;
}
// 这里为对象转字符串形式
let string = '';
for (const i in customStyle) {
// 驼峰转为中划线的形式否则css内联样式无法识别驼峰样式属性名
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase();
string += `${key}:${customStyle[i]};`;
}
// 去除两端空格
return trim(string);
}
/**
* @description 添加单位如果有rpxupx%px等单位结尾或者值为auto直接返回否则加上px单位结尾
* @param {string|number} value 需要添加单位的值
* @param {string} unit 添加的单位名 比如px
*/
export function addUnit(value = 'auto', unit = 'px') {
value = String(value);
return test.number(value) ? `${value}${unit}` : value;
}
/**
* @description 深度克隆
* @param {object} obj 需要深度克隆的对象
* @returns {*} 克隆后的对象或者原值(不是对象)
*/
function deepClone(obj) {
// 对常见的“非”值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj;
if (typeof obj !== 'object' && typeof obj !== 'function') {
// 原始类型直接返回
return obj;
}
const o = test.array(obj) ? [] : {};
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
}
}
return o;
}
/**
* @description JS对象深度合并
* @param {object} target 需要拷贝的对象
* @param {object} source 拷贝的来源对象
* @returns {object|boolean} 深度合并后的对象或者false入参有不是对象
*/
export function deepMerge(target = {}, source = {}) {
target = deepClone(target);
if (typeof target !== 'object' || typeof source !== 'object') return false;
for (const prop in source) {
if (!source.hasOwnProperty(prop)) continue;
if (prop in target) {
if (typeof target[prop] !== 'object') {
target[prop] = source[prop];
} else if (typeof source[prop] !== 'object') {
target[prop] = source[prop];
} else if (target[prop].concat && source[prop].concat) {
target[prop] = target[prop].concat(source[prop]);
} else {
target[prop] = deepMerge(target[prop], source[prop]);
}
} else {
target[prop] = source[prop];
}
}
return target;
}
/**
* @description error提示
* @param {*} err 错误内容
*/
function error(err) {
// 开发环境才提示,生产环境不会提示
if (process.env.NODE_ENV === 'development') {
console.error(`SheepJS:${err}`);
}
}
/**
* @description 打乱数组
* @param {array} array 需要打乱的数组
* @returns {array} 打乱后的数组
*/
function randomArray(array = []) {
// 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.05大于或者小于0
return array.sort(() => Math.random() - 0.5);
}
// padStart 的 polyfill因为某些机型或情况还无法支持es7的padStart比如电脑版的微信小程序
// 所以这里做一个兼容polyfill的兼容处理
if (!String.prototype.padStart) {
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function (maxLength, fillString = ' ') {
if (Object.prototype.toString.call(fillString) !== '[object String]') {
throw new TypeError('fillString must be String');
}
const str = this;
// 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉
if (str.length >= maxLength) return String(str);
const fillLength = maxLength - str.length;
let times = Math.ceil(fillLength / fillString.length);
while ((times >>= 1)) {
fillString += fillString;
if (times === 1) {
fillString += fillString;
}
}
return fillString.slice(0, fillLength) + str;
};
}
/**
* @description 格式化时间
* @param {String|Number} dateTime 需要格式化的时间戳
* @param {String} fmt 格式化规则 yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合 默认yyyy-mm-dd
* @returns {string} 返回格式化后的字符串
*/
function timeFormat(dateTime = null, formatStr = 'yyyy-mm-dd') {
let date;
// 若传入时间为假值,则取当前时间
if (!dateTime) {
date = new Date();
}
// 若为unix秒时间戳则转为毫秒时间戳逻辑有点奇怪但不敢改以保证历史兼容
else if (/^\d{10}$/.test(dateTime?.toString().trim())) {
date = new Date(dateTime * 1000);
}
// 若用户传入字符串格式时间戳new Date无法解析需做兼容
else if (typeof dateTime === 'string' && /^\d+$/.test(dateTime.trim())) {
date = new Date(Number(dateTime));
}
// 其他都认为符合 RFC 2822 规范
else {
// 处理平台性差异在Safari/Webkit中new Date仅支持/作为分割符的字符串时间
date = new Date(typeof dateTime === 'string' ? dateTime.replace(/-/g, '/') : dateTime);
}
const timeSource = {
y: date.getFullYear().toString(), // 年
m: (date.getMonth() + 1).toString().padStart(2, '0'), // 月
d: date.getDate().toString().padStart(2, '0'), // 日
h: date.getHours().toString().padStart(2, '0'), // 时
M: date.getMinutes().toString().padStart(2, '0'), // 分
s: date.getSeconds().toString().padStart(2, '0'), // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const key in timeSource) {
const [ret] = new RegExp(`${key}+`).exec(formatStr) || [];
if (ret) {
// 年可能只需展示两位
const beginIndex = key === 'y' && ret.length === 2 ? 2 : 0;
formatStr = formatStr.replace(ret, timeSource[key].slice(beginIndex));
}
}
return formatStr;
}
/**
* @description 时间戳转为多久之前
* @param {String|Number} timestamp 时间戳
* @param {String|Boolean} format
* 格式化规则如果为时间格式字符串,超出一定时间范围,返回固定的时间格式;
* 如果为布尔值false无论什么时间都返回多久以前的格式
* @returns {string} 转化后的内容
*/
function timeFrom(timestamp = null, format = 'yyyy-mm-dd') {
if (timestamp == null) timestamp = Number(new Date());
timestamp = parseInt(timestamp);
// 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位)
if (timestamp.toString().length == 10) timestamp *= 1000;
let timer = new Date().getTime() - timestamp;
timer = parseInt(timer / 1000);
// 如果小于5分钟,则返回"刚刚",其他以此类推
let tips = '';
switch (true) {
case timer < 300:
tips = '刚刚';
break;
case timer >= 300 && timer < 3600:
tips = `${parseInt(timer / 60)}分钟前`;
break;
case timer >= 3600 && timer < 86400:
tips = `${parseInt(timer / 3600)}小时前`;
break;
case timer >= 86400 && timer < 2592000:
tips = `${parseInt(timer / 86400)}天前`;
break;
default:
// 如果format为false则无论什么时间戳都显示xx之前
if (format === false) {
if (timer >= 2592000 && timer < 365 * 86400) {
tips = `${parseInt(timer / (86400 * 30))}个月前`;
} else {
tips = `${parseInt(timer / (86400 * 365))}年前`;
}
} else {
tips = timeFormat(timestamp, format);
}
}
return tips;
}
/**
* @description 去除空格
* @param String str 需要去除空格的字符串
* @param String pos both(左右)|left|right|all 默认both
*/
function trim(str, pos = 'both') {
str = String(str);
if (pos == 'both') {
return str.replace(/^\s+|\s+$/g, '');
}
if (pos == 'left') {
return str.replace(/^\s*/, '');
}
if (pos == 'right') {
return str.replace(/(\s*$)/g, '');
}
if (pos == 'all') {
return str.replace(/\s+/g, '');
}
return str;
}
/**
* @description 对象转url参数
* @param {object} data,对象
* @param {Boolean} isPrefix,是否自动加上"?"
* @param {string} arrayFormat 规则 indices|brackets|repeat|comma
*/
function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
const prefix = isPrefix ? '?' : '';
const _result = [];
if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1)
arrayFormat = 'brackets';
for (const key in data) {
const value = data[key];
// 去掉为空的参数
if (['', undefined, null].indexOf(value) >= 0) {
continue;
}
// 如果值为数组,另行处理
if (value.constructor === Array) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(`${key}[${i}]=${value[i]}`);
}
break;
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`);
});
break;
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach((_value) => {
_result.push(`${key}=${_value}`);
});
break;
case 'comma':
// 结果: ids=1,2,3
let commaStr = '';
value.forEach((_value) => {
commaStr += (commaStr ? ',' : '') + _value;
});
_result.push(`${key}=${commaStr}`);
break;
default:
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`);
});
}
} else {
_result.push(`${key}=${value}`);
}
}
return _result.length ? prefix + _result.join('&') : '';
}
/**
* 显示消息提示框
* @param {String} title 提示的内容,长度与 icon 取值有关。
* @param {Number} duration 提示的延迟时间单位毫秒默认2000
*/
function toast(title, duration = 2000) {
uni.showToast({
title: String(title),
icon: 'none',
duration,
});
}
/**
* @description 根据主题type值,获取对应的图标
* @param {String} type 主题名称,primary|info|error|warning|success
* @param {boolean} fill 是否使用fill填充实体的图标
*/
function type2icon(type = 'success', fill = false) {
// 如果非预置值,默认为success
if (['primary', 'info', 'error', 'warning', 'success'].indexOf(type) == -1) type = 'success';
let iconName = '';
// 目前(2019-12-12),info和primary使用同一个图标
switch (type) {
case 'primary':
iconName = 'info-circle';
break;
case 'info':
iconName = 'info-circle';
break;
case 'error':
iconName = 'close-circle';
break;
case 'warning':
iconName = 'error-circle';
break;
case 'success':
iconName = 'checkmark-circle';
break;
default:
iconName = 'checkmark-circle';
}
// 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的
if (fill) iconName += '-fill';
return iconName;
}
/**
* @description 数字格式化
* @param {number|string} number 要格式化的数字
* @param {number} decimals 保留几位小数
* @param {string} decimalPoint 小数点符号
* @param {string} thousandsSeparator 千分位符号
* @returns {string} 格式化后的数字
*/
function priceFormat(number, decimals = 0, decimalPoint = '.', thousandsSeparator = ',') {
number = `${number}`.replace(/[^0-9+-Ee.]/g, '');
const n = !isFinite(+number) ? 0 : +number;
const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
const sep = typeof thousandsSeparator === 'undefined' ? ',' : thousandsSeparator;
const dec = typeof decimalPoint === 'undefined' ? '.' : decimalPoint;
let s = '';
s = (prec ? round(n, prec) + '' : `${Math.round(n)}`).split('.');
const re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, `$1${sep}$2`);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
/**
* @description 获取duration值
* 如果带有ms或者s直接返回如果大于一定值认为是ms单位小于一定值认为是s单位
* 比如以30位阈值那么300大于30可以理解为用户想要的是300ms而不是想花300s去执行一个动画
* @param {String|number} value 比如: "1s"|"100ms"|1|100
* @param {boolean} unit 提示: 如果是false 默认返回number
* @return {string|number}
*/
function getDuration(value, unit = true) {
const valueNum = parseInt(value);
if (unit) {
if (/s$/.test(value)) return value;
return value > 30 ? `${value}ms` : `${value}s`;
}
if (/ms$/.test(value)) return valueNum;
if (/s$/.test(value)) return valueNum > 30 ? valueNum : valueNum * 1000;
return valueNum;
}
/**
* @description 日期的月或日补零操作
* @param {String} value 需要补零的值
*/
function padZero(value) {
return `00${value}`.slice(-2);
}
/**
* @description 获取某个对象下的属性,用于通过类似'a.b.c'的形式去获取一个对象的的属性的形式
* @param {object} obj 对象
* @param {string} key 需要获取的属性字段
* @returns {*}
*/
function getProperty(obj, key) {
if (!obj) {
return;
}
if (typeof key !== 'string' || key === '') {
return '';
}
if (key.indexOf('.') !== -1) {
const keys = key.split('.');
let firstObj = obj[keys[0]] || {};
for (let i = 1; i < keys.length; i++) {
if (firstObj) {
firstObj = firstObj[keys[i]];
}
}
return firstObj;
}
return obj[key];
}
/**
* @description 设置对象的属性值,如果'a.b.c'的形式进行设置
* @param {object} obj 对象
* @param {string} key 需要设置的属性
* @param {string} value 设置的值
*/
function setProperty(obj, key, value) {
if (!obj) {
return;
}
// 递归赋值
const inFn = function (_obj, keys, v) {
// 最后一个属性key
if (keys.length === 1) {
_obj[keys[0]] = v;
return;
}
// 0~length-1个key
while (keys.length > 1) {
const k = keys[0];
if (!_obj[k] || typeof _obj[k] !== 'object') {
_obj[k] = {};
}
const key = keys.shift();
// 自调用判断是否存在属性,不存在则自动创建对象
inFn(_obj[k], keys, v);
}
};
if (typeof key !== 'string' || key === '') {
} else if (key.indexOf('.') !== -1) {
// 支持多层级赋值操作
const keys = key.split('.');
inFn(obj, keys, value);
} else {
obj[key] = value;
}
}
/**
* @description 获取当前页面路径
*/
function page() {
const pages = getCurrentPages();
// 某些特殊情况下(比如页面进行redirectTo时的一些时机)pages可能为空数组
return `/${pages[pages.length - 1]?.route || ''}`;
}
/**
* @description 获取当前路由栈实例数组
*/
function pages() {
const pages = getCurrentPages();
return pages;
}
/**
* 获取H5-真实根地址 兼容hash+history模式
*/
export function getRootUrl() {
let url = '';
// #ifdef H5
url = location.origin;
// + location.pathname;
if (location.hash !== '') {
url += '#/';
} else {
url += '/';
}
// #endif
return url;
}
/**
* copyText 多端复制文本
*/
export function copyText(text) {
// #ifndef H5
uni.setClipboardData({
data: text,
success: function () {
toast('复制成功!');
},
fail: function () {
toast('复制失败!');
},
});
// #endif
// #ifdef H5
var createInput = document.createElement('textarea');
createInput.value = text;
document.body.appendChild(createInput);
createInput.select();
document.execCommand('Copy');
createInput.className = 'createInput';
createInput.style.display = 'none';
toast('复制成功');
// #endif
}
export default {
range,
getPx,
sleep,
os,
random,
guid,
$parent,
addStyle,
addUnit,
deepClone,
deepMerge,
error,
randomArray,
timeFormat,
timeFrom,
trim,
queryParams,
toast,
type2icon,
priceFormat,
getDuration,
padZero,
getProperty,
setProperty,
page,
pages,
test,
getRootUrl,
copyText,
};

View File

@@ -0,0 +1,13 @@
const HOME_MENU_URL = '/pages/index/menu';
export function navigateAfterLogin({ delay = 500 } = {}) {
setTimeout(() => {
uni.reLaunch({
url: HOME_MENU_URL,
});
}, delay);
}
export default {
navigateAfterLogin,
};

285
sheep/helper/test.js Normal file
View File

@@ -0,0 +1,285 @@
/**
* 验证电子邮箱格式
*/
function email(value) {
return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value);
}
/**
* 验证手机格式
*/
function mobile(value) {
return /^1[23456789]\d{9}$/.test(value);
}
/**
* 验证URL格式
*/
function url(value) {
return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/.test(
value,
);
}
/**
* 验证日期格式
*/
function date(value) {
if (!value) return false;
// 判断是否数值或者字符串数值(意味着为时间戳)转为数值否则new Date无法识别字符串时间戳
if (number(value)) value = +value;
return !/Invalid|NaN/.test(new Date(value).toString());
}
/**
* 验证ISO类型的日期格式
*/
function dateISO(value) {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value);
}
/**
* 验证十进制数字
*/
function number(value) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value);
}
/**
* 验证字符串
*/
function string(value) {
return typeof value === 'string';
}
/**
* 验证整数
*/
function digits(value) {
return /^\d+$/.test(value);
}
/**
* 验证身份证号码
*/
function idCard(value) {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value);
}
/**
* 是否车牌号
*/
function carNo(value) {
// 新能源车牌
const xreg =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
// 旧车牌
const creg =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
if (value.length === 7) {
return creg.test(value);
}
if (value.length === 8) {
return xreg.test(value);
}
return false;
}
/**
* 金额,只允许2位小数
*/
function amount(value) {
// 金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value);
}
/**
* 中文
*/
function chinese(value) {
const reg = /^[\u4e00-\u9fa5]+$/gi;
return reg.test(value);
}
/**
* 只能输入字母
*/
function letter(value) {
return /^[a-zA-Z]*$/.test(value);
}
/**
* 只能是字母或者数字
*/
function enOrNum(value) {
// 英文或者数字
const reg = /^[0-9a-zA-Z]*$/g;
return reg.test(value);
}
/**
* 验证是否包含某个值
*/
function contains(value, param) {
return value.indexOf(param) >= 0;
}
/**
* 验证一个值范围[min, max]
*/
function range(value, param) {
return value >= param[0] && value <= param[1];
}
/**
* 验证一个长度范围[min, max]
*/
function rangeLength(value, param) {
return value.length >= param[0] && value.length <= param[1];
}
/**
* 是否固定电话
*/
function landline(value) {
const reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/;
return reg.test(value);
}
/**
* 判断是否为空
*/
function empty(value) {
switch (typeof value) {
case 'undefined':
return true;
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
break;
case 'boolean':
if (!value) return true;
break;
case 'number':
if (value === 0 || isNaN(value)) return true;
break;
case 'object':
if (value === null || value.length === 0) return true;
for (const i in value) {
return false;
}
return true;
}
return false;
}
/**
* 是否json字符串
*/
function jsonString(value) {
if (typeof value === 'string') {
try {
const obj = JSON.parse(value);
if (typeof obj === 'object' && obj) {
return true;
}
return false;
} catch (e) {
return false;
}
}
return false;
}
/**
* 是否数组
*/
function array(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value);
}
return Object.prototype.toString.call(value) === '[object Array]';
}
/**
* 是否对象
*/
function object(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
/**
* 是否短信验证码
*/
function code(value, len = 6) {
return new RegExp(`^\\d{${len}}$`).test(value);
}
/**
* 是否函数方法
* @param {Object} value
*/
function func(value) {
return typeof value === 'function';
}
/**
* 是否promise对象
* @param {Object} value
*/
function promise(value) {
return object(value) && func(value.then) && func(value.catch);
}
/** 是否图片格式
* @param {Object} value
*/
function image(value) {
const newValue = value.split('?')[0];
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i;
return IMAGE_REGEXP.test(newValue);
}
/**
* 是否视频格式
* @param {Object} value
*/
function video(value) {
const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i;
return VIDEO_REGEXP.test(value);
}
/**
* 是否为正则对象
* @param {Object}
* @return {Boolean}
*/
function regExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]';
}
export default {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
isEmpty: empty,
isNumber: number,
jsonString,
landline,
object,
array,
code,
};

125
sheep/helper/theme.js Normal file
View File

@@ -0,0 +1,125 @@
// 主题工具函数
import { themeConfig } from '@/sheep/config/theme';
/**
* 获取主题色
* @param {string} type - 主题色类型 'main' | 'light' | 'dark'
* @returns {string} 颜色值
*/
export function getThemeColor(type = 'main') {
return themeConfig.primary[type] || themeConfig.primary.main;
}
/**
* 获取主题色透明度版本
* @param {number} opacity - 透明度 1-5
* @returns {string} rgba颜色值
*/
export function getThemeColorWithOpacity(opacity = 1) {
return themeConfig.primary.opacity[opacity] || themeConfig.primary.opacity[1];
}
/**
* 获取语义化颜色
* @param {string} type - 颜色类型 'success' | 'warning' | 'danger' | 'info'
* @returns {string} 颜色值
*/
export function getSemanticColor(type) {
return themeConfig.colors[type] || themeConfig.colors.info;
}
/**
* 获取文本颜色
* @param {string} type - 文本类型 'primary' | 'regular' | 'secondary' | 'placeholder'
* @returns {string} 颜色值
*/
export function getTextColor(type = 'primary') {
return themeConfig.colors.text[type] || themeConfig.colors.text.primary;
}
/**
* 获取边框颜色
* @param {string} type - 边框类型 'base' | 'light' | 'lighter' | 'extra_light'
* @returns {string} 颜色值
*/
export function getBorderColor(type = 'base') {
return themeConfig.colors.border[type] || themeConfig.colors.border.base;
}
/**
* 获取背景颜色
* @param {string} type - 背景类型 'base' | 'page' | 'card'
* @returns {string} 颜色值
*/
export function getBackgroundColor(type = 'base') {
return themeConfig.colors.background[type] || themeConfig.colors.background.base;
}
/**
* 设置主题色
* @param {string} color - 新的主题色
*/
export function setThemeColor(color) {
// 更新主题配置
themeConfig.primary.main = color;
// 生成相关颜色
themeConfig.primary.light = lightenColor(color, 20);
themeConfig.primary.dark = darkenColor(color, 20);
// 生成透明度颜色
const rgb = hexToRgb(color);
for (let i = 1; i <= 5; i++) {
themeConfig.primary.opacity[i] = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.${i})`;
}
}
/**
* 十六进制颜色转RGB
*/
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
/**
* 颜色变亮
*/
function lightenColor(hex, percent) {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
const factor = percent / 100;
rgb.r = Math.round(rgb.r + (255 - rgb.r) * factor);
rgb.g = Math.round(rgb.g + (255 - rgb.g) * factor);
rgb.b = Math.round(rgb.b + (255 - rgb.b) * factor);
return `#${rgb.r.toString(16).padStart(2, '0')}${rgb.g.toString(16).padStart(2, '0')}${rgb.b.toString(16).padStart(2, '0')}`;
}
/**
* 颜色变暗
*/
function darkenColor(hex, percent) {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
const factor = percent / 100;
rgb.r = Math.round(rgb.r * (1 - factor));
rgb.g = Math.round(rgb.g * (1 - factor));
rgb.b = Math.round(rgb.b * (1 - factor));
return `#${rgb.r.toString(16).padStart(2, '0')}${rgb.g.toString(16).padStart(2, '0')}${rgb.b.toString(16).padStart(2, '0')}`;
}
export default {
getThemeColor,
getThemeColorWithOpacity,
getSemanticColor,
getTextColor,
getBorderColor,
getBackgroundColor,
setThemeColor
};

31
sheep/helper/throttle.js Normal file
View File

@@ -0,0 +1,31 @@
let timer;
let flag;
/**
* 节流原理:在一定时间内,只能触发一次
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
function throttle(func, wait = 500, immediate = true) {
if (immediate) {
if (!flag) {
flag = true;
// 如果是立即执行则在wait毫秒内开始时执行
typeof func === 'function' && func();
timer = setTimeout(() => {
flag = false;
}, wait);
} else {
}
} else if (!flag) {
flag = true;
// 如果是非立即执行则在wait毫秒内的结束处执行
timer = setTimeout(() => {
flag = false;
typeof func === 'function' && func();
}, wait);
}
}
export default throttle;

67
sheep/helper/tools.js Normal file
View File

@@ -0,0 +1,67 @@
import router from '@/sheep/router';
export default {
/**
* 打电话
* @param {String<Number>} phoneNumber - 数字字符串
*/
callPhone(phoneNumber = '') {
let num = phoneNumber.toString();
uni.makePhoneCall({
phoneNumber: num,
fail(err) {
console.log('makePhoneCall出错', err);
},
});
},
/**
* 微信头像
* @param {String} url -图片地址
*/
checkMPUrl(url) {
// #ifdef MP
if (
url.substring(0, 4) === 'http' &&
url.substring(0, 5) !== 'https' &&
url.substring(0, 12) !== 'http://store' &&
url.substring(0, 10) !== 'http://tmp' &&
url.substring(0, 10) !== 'http://usr'
) {
url = 'https' + url.substring(4, url.length);
}
// #endif
return url;
},
/**
* getUuid 生成唯一id
*/
getUuid(len = 32, firstU = true, radix = null) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid = [];
radix = radix || chars.length;
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
let r;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
if (firstU) {
uuid.shift();
return `u${uuid.join('')}`;
}
return uuid.join('');
},
};

336
sheep/helper/utils.js Normal file
View File

@@ -0,0 +1,336 @@
export function isArray(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === '[object Array]';
}
}
export function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
export function isNumber(value) {
return !isNaN(Number(value));
}
export function isFunction(value) {
return typeof value == 'function';
}
export function isString(value) {
return typeof value == 'string';
}
export function isEmpty(value) {
if (value === '' || value === undefined || value === null) {
return true;
}
if (isArray(value)) {
return value.length === 0;
}
if (isObject(value)) {
return Object.keys(value).length === 0;
}
return false;
}
export function isBoolean(value) {
return typeof value === 'boolean';
}
export function last(data) {
if (isArray(data) || isString(data)) {
return data[data.length - 1];
}
}
export function cloneDeep(obj) {
const d = isArray(obj) ? [...obj] : {};
if (isObject(obj)) {
for (const key in obj) {
if (obj[key]) {
if (obj[key] && typeof obj[key] === 'object') {
d[key] = cloneDeep(obj[key]);
} else {
d[key] = obj[key];
}
}
}
}
return d;
}
export function clone(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
export function deepMerge(a, b) {
let k;
for (k in b) {
a[k] = a[k] && a[k].toString() === '[object Object]' ? deepMerge(a[k], b[k]) : (a[k] = b[k]);
}
return a;
}
export function contains(parent, node) {
while (node && (node = node.parentNode)) if (node === parent) return true;
return false;
}
export function orderBy(list, key) {
return list.sort((a, b) => a[key] - b[key]);
}
export function deepTree(list) {
const newList = [];
const map = {};
list.forEach((e) => (map[e.id] = e));
list.forEach((e) => {
const parent = map[e.parentId];
if (parent) {
(parent.children || (parent.children = [])).push(e);
} else {
newList.push(e);
}
});
const fn = (list) => {
list.map((e) => {
if (e.children instanceof Array) {
e.children = orderBy(e.children, 'orderNum');
fn(e.children);
}
});
};
fn(newList);
return orderBy(newList, 'orderNum');
}
export function revDeepTree(list = []) {
const d = [];
let id = 0;
const deep = (list, parentId) => {
list.forEach((e) => {
if (!e.id) {
e.id = id++;
}
e.parentId = parentId;
d.push(e);
if (e.children && isArray(e.children)) {
deep(e.children, e.id);
}
});
};
deep(list || [], null);
return d;
}
export function basename(path) {
let index = path.lastIndexOf('/');
index = index > -1 ? index : path.lastIndexOf('\\');
if (index < 0) {
return path;
}
return path.substring(index + 1);
}
export function isWxBrowser() {
const ua = navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
return true;
} else {
return false;
}
}
/**
* @description 如果value小于min取min如果value大于max取max
* @param {number} min
* @param {number} max
* @param {number} value
*/
export function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)));
}
import dayjs from 'dayjs';
/**
* 将一个整数转换为分数保留两位小数
* @param {number | string | undefined} num 整数
* @return {number} 分数
*/
export const formatToFraction = (num) => {
if (typeof num === 'undefined') return 0;
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num;
return parseFloat((parsedNumber / 100).toFixed(2));
};
/**
* 将一个数转换为 1.00 这样
* 数据呈现的时候使用
*
* @param {number | string | undefined} num 整数
* @return {string} 分数
*/
export const floatToFixed2 = (num) => {
let str = '0.00';
if (typeof num === 'undefined') {
return str;
}
const f = formatToFraction(num);
const decimalPart = f.toString().split('.')[1];
const len = decimalPart ? decimalPart.length : 0;
switch (len) {
case 0:
str = f.toString() + '.00';
break;
case 1:
str = f.toString() + '.0';
break;
case 2:
str = f.toString();
break;
}
return str;
};
/**
* 时间日期转换
* @param {dayjs.ConfigType} date 当前时间new Date() 格式
* @param {string} format 需要转换的时间格式字符串
* @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd`
* @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ"
* @description format 星期:"YYYY-mm-dd HH:MM:SS WWW"
* @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ"
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
* @returns {string} 返回拼接后的时间字符串
*/
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
// 日期不存在,则返回空
if (!date) {
return '';
}
// 日期存在,则进行格式化
if (format === undefined) {
format = 'YYYY-MM-DD HH:mm:ss';
}
return dayjs(date).format(format);
}
/**
* 构造树型结构数据
*
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
* @param {*} rootId 根Id 默认 0
*/
export function handleTree(
data,
id = 'id',
parentId = 'parentId',
children = 'children',
rootId = 0,
) {
// 对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data));
// 循环所有项
const treeData = cloneData.filter((father) => {
let branchArr = cloneData.filter((child) => {
//返回每一项的子级数组
return father[id] === child[parentId];
});
branchArr.length > 0 ? (father.children = branchArr) : '';
//返回第一层
return father[parentId] === rootId;
});
return treeData !== '' ? treeData : data;
}
/**
* 重置分页对象
*
* @param pagination 分页对象
*/
export function resetPagination(pagination) {
pagination.list = [];
pagination.total = 0;
pagination.pageNo = 1;
}
/**
* 将值复制到目标对象且以目标对象属性为准target: {a:1} source:{a:2,b:3} 结果为:{a:2}
* @param target 目标对象
* @param source 源对象
*/
export const copyValueToTarget = (target, source) => {
const newObj = Object.assign({}, target, source);
// 删除多余属性
Object.keys(newObj).forEach((key) => {
// 如果不是target中的属性则删除
if (Object.keys(target).indexOf(key) === -1) {
delete newObj[key];
}
});
// 更新目标对象值
Object.assign(target, newObj);
};
/**
* 解析 JSON 字符串
*
* @param str
*/
export function jsonParse(str) {
try {
return JSON.parse(str);
} catch (e) {
console.warn(`str[${str}] 不是一个 JSON 字符串`);
return str;
}
}
/**
* 获得当前周的开始和结束时间
*/
export function getWeekTimes() {
const today = new Date();
const dayOfWeek = today.getDay();
return [
new Date(today.getFullYear(), today.getMonth(), today.getDate() - dayOfWeek, 0, 0, 0),
new Date(today.getFullYear(), today.getMonth(), today.getDate() + (6 - dayOfWeek), 23, 59, 59),
];
}
/**
* 获得当前月的开始和结束时间
*/
export function getMonthTimes() {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth();
const startDate = new Date(year, month, 1, 0, 0, 0);
const nextMonth = new Date(year, month + 1, 1);
const endDate = new Date(nextMonth.getTime() - 1);
return [startDate, endDate];
}

166
sheep/hooks/useModal.js Normal file
View File

@@ -0,0 +1,166 @@
import $store from '@/sheep/store';
import $helper from '@/sheep/helper';
import dayjs from 'dayjs';
import { ref } from 'vue';
import test from '@/sheep/helper/test.js';
import AuthUtil from '@/sheep/api/system/auth';
import sheep from '@/sheep';
// 打开授权弹框
export function showAuthModal(type = 'accountLogin') {
sheep.$router.go('/pages/login/index', { authType: type });
}
// 关闭授权弹框
export function closeAuthModal() {
sheep.$router.back();
}
// 打开分享弹框
export function showShareModal() {
$store('modal').$patch((state) => {
state.share = true;
});
}
// 关闭分享弹框
export function closeShareModal() {
$store('modal').$patch((state) => {
state.share = false;
});
}
// 打开快捷菜单
export function showMenuTools() {
$store('modal').$patch((state) => {
state.menu = true;
});
}
// 关闭快捷菜单
export function closeMenuTools() {
$store('modal').$patch((state) => {
state.menu = false;
});
}
// 发送短信验证码 60秒
export function getSmsCode(event, mobile) {
const modalStore = $store('modal');
const lastSendTimer = modalStore.lastTimer[event];
if (typeof lastSendTimer === 'undefined') {
$helper.toast('短信发送事件错误');
return;
}
const duration = dayjs().unix() - lastSendTimer;
const canSend = duration >= 60;
if (!canSend) {
$helper.toast('请稍后再试');
return;
}
// 只有 mobile 非空时才校验。因为部分场景(修改密码),不需要输入手机
if (mobile && !test.mobile(mobile)) {
$helper.toast('手机号码格式不正确');
return;
}
// 发送验证码 + 更新上次发送验证码时间
let scene = -1;
switch (event) {
case 'resetPassword':
scene = 4;
break;
case 'changePassword':
scene = 3;
break;
case 'changeMobile':
scene = 2;
break;
case 'smsLogin':
scene = 1;
break;
}
AuthUtil.sendSmsCode(mobile, scene).then((res) => {
if (res.code === 0) {
modalStore.$patch((state) => {
state.lastTimer[event] = dayjs().unix();
});
} else {
$helper.toast(res.msg || '验证码发送失败,请稍后重试');
}
}).catch((error) => {
$helper.toast('验证码发送失败,请稍后重试');
});
}
// 获取短信验证码倒计时 -- 60秒
export function getSmsTimer(event, mobile = '') {
const modalStore = $store('modal');
const lastSendTimer = modalStore.lastTimer[event];
if (typeof lastSendTimer === 'undefined') {
return '获取验证码';
}
const currentTime = dayjs().unix();
const duration = currentTime - lastSendTimer;
const canSend = duration >= 60;
if (canSend) {
return '获取验证码';
}
const remainingTime = 60 - duration;
return `${remainingTime}`;
}
// 创建响应式的短信验证码计时器
export function useSmsTimer(event) {
const modalStore = $store('modal');
const timer = ref('获取验证码');
const updateTimer = () => {
const lastSendTimer = modalStore.lastTimer[event];
if (typeof lastSendTimer === 'undefined') {
timer.value = '获取验证码';
return;
}
const currentTime = dayjs().unix();
const duration = currentTime - lastSendTimer;
const canSend = duration >= 60;
if (canSend) {
timer.value = '获取验证码';
} else {
const remainingTime = 60 - duration;
timer.value = `${remainingTime}`;
}
};
// 初始化
updateTimer();
// 每秒更新一次
const intervalId = setInterval(updateTimer, 1000);
// 清理定时器的函数
const cleanup = () => {
clearInterval(intervalId);
};
return { timer, cleanup };
}
// 记录广告弹框历史
export function saveAdvHistory(adv) {
const modal = $store('modal');
modal.$patch((state) => {
if (!state.advHistory.includes(adv.imgUrl)) {
state.advHistory.push(adv.imgUrl);
}
});
}

52
sheep/index.js Normal file
View File

@@ -0,0 +1,52 @@
import $api from '@/sheep/api';
import $url from '@/sheep/url';
import $router from '@/sheep/router';
import $platform from '@/sheep/platform';
import $helper from '@/sheep/helper';
import zIndex from '@/sheep/config/zIndex.js';
import $store from '@/sheep/store';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import duration from 'dayjs/plugin/duration';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
dayjs.extend(relativeTime);
dayjs.extend(duration);
const sheep = {
$api,
$store,
$url,
$router,
$platform,
$helper,
$zIndex: zIndex,
};
// 加载Shopro底层依赖
export async function ShoproInit() {
// 应用初始化
await $store('app').init();
// 平台初始化加载(各平台provider提供不同的加载流程)
$platform.load();
if (process.env.NODE_ENV === 'development') {
ShoproDebug();
}
}
// 开发模式
function ShoproDebug() {
// 开发环境引入vconsole调试
// #ifdef H5
// import("vconsole").then(vconsole => {
// new vconsole.default();
// });
// #endif
// 同步前端页面到后端
// console.log(ROUTES)
}
export default sheep;

View File

@@ -0,0 +1,32 @@
const fs = require('fs');
const manifestPath = process.env.UNI_INPUT_DIR + '/manifest.json';
let Manifest = fs.readFileSync(manifestPath, {
encoding: 'utf-8'
});
function mpliveMainfestPlugin(isOpen) {
if (process.env.UNI_PLATFORM !== 'mp-weixin') return;
const manifestData = JSON.parse(Manifest)
if (isOpen === '0') {
delete manifestData['mp-weixin'].plugins['live-player-plugin'];
}
if (isOpen === '1') {
manifestData['mp-weixin'].plugins['live-player-plugin'] = {
"version": "1.3.5",
"provider": "wx2b03c6e691cd7370"
}
}
Manifest = JSON.stringify(manifestData, null, 2)
fs.writeFileSync(manifestPath, Manifest, {
"flag": "w"
})
}
export default mpliveMainfestPlugin

244
sheep/libs/permission.js Normal file
View File

@@ -0,0 +1,244 @@
/// null = 未请求1 = 已允许0 = 拒绝|受限, 2 = 系统未开启
var isIOS;
function album() {
var result = 0;
var PHPhotoLibrary = plus.ios.import('PHPhotoLibrary');
var authStatus = PHPhotoLibrary.authorizationStatus();
if (authStatus === 0) {
result = null;
} else if (authStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
function camera() {
var result = 0;
var AVCaptureDevice = plus.ios.import('AVCaptureDevice');
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
if (authStatus === 0) {
result = null;
} else if (authStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
function location() {
var result = 0;
var cllocationManger = plus.ios.import('CLLocationManager');
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
if (!enable) {
result = 2;
} else if (status === 0) {
result = null;
} else if (status === 3 || status === 4) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(cllocationManger);
return result;
}
function push() {
var result = 0;
var UIApplication = plus.ios.import('UIApplication');
var app = UIApplication.sharedApplication();
var enabledTypes = 0;
if (app.currentUserNotificationSettings) {
var settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute('types');
if (enabledTypes == 0) {
result = 0;
console.log('推送权限没有开启');
} else {
result = 1;
console.log('已经开启推送功能!');
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
result = 3;
console.log('推送权限没有开启!');
} else {
result = 4;
console.log('已经开启推送功能!');
}
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
function contact() {
var result = 0;
var CNContactStore = plus.ios.import('CNContactStore');
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus === 0) {
result = null;
} else if (cnAuthStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(CNContactStore);
return result;
}
function record() {
var result = null;
var avaudiosession = plus.ios.import('AVAudioSession');
var avaudio = avaudiosession.sharedInstance();
var status = avaudio.recordPermission();
console.log('permissionStatus:' + status);
if (status === 1970168948) {
result = null;
} else if (status === 1735552628) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(avaudiosession);
return result;
}
function calendar() {
var result = null;
var EKEventStore = plus.ios.import('EKEventStore');
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = 1;
console.log('日历权限已经开启');
} else {
console.log('日历权限没有开启');
}
plus.ios.deleteObject(EKEventStore);
return result;
}
function memo() {
var result = null;
var EKEventStore = plus.ios.import('EKEventStore');
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = 1;
console.log('备忘录权限已经开启');
} else {
console.log('备忘录权限没有开启');
}
plus.ios.deleteObject(EKEventStore);
return result;
}
function requestIOS(permissionID) {
return new Promise((resolve, reject) => {
switch (permissionID) {
case 'push':
resolve(push());
break;
case 'location':
resolve(location());
break;
case 'record':
resolve(record());
break;
case 'camera':
resolve(camera());
break;
case 'album':
resolve(album());
break;
case 'contact':
resolve(contact());
break;
case 'calendar':
resolve(calendar());
break;
case 'memo':
resolve(memo());
break;
default:
resolve(0);
break;
}
});
}
function requestAndroid(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID],
function (resultObj) {
var result = 0;
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:' + grantedPermission);
result = 1;
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:' + deniedPresentPermission);
result = 0;
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
result = -1;
}
resolve(result);
},
function (error) {
console.log('result error: ' + error.message);
resolve({
code: error.code,
message: error.message,
});
},
);
});
}
function gotoAppPermissionSetting() {
if (permission.isIOS) {
var UIApplication = plus.ios.import('UIApplication');
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import('NSURL');
var setting2 = NSURL2.URLWithString('app-settings:');
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var Uri = plus.android.importClass('android.net.Uri');
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts('package', mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
const permission = {
get isIOS() {
return typeof isIOS === 'boolean' ? isIOS : (isIOS = uni.getDeviceInfo().platform === 'ios');
},
requestIOS: requestIOS,
requestAndroid: requestAndroid,
gotoAppSetting: gotoAppPermissionSetting,
};
export default permission;

193
sheep/libs/sdk-h5-weixin.js Normal file
View File

@@ -0,0 +1,193 @@
/**
* 本模块封装微信浏览器下的一些方法。
* 更多微信网页开发sdk方法,详见:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
* 有 the permission value is offline verifying 报错请参考 @see https://segmentfault.com/a/1190000042289419 解决
*/
import jweixin from 'weixin-js-sdk';
import $helper from '@/sheep/helper';
import AuthUtil from '@/sheep/api/system/auth';
let configSuccess = false;
export default {
// 判断是否在微信中
isWechat() {
const ua = window.navigator.userAgent.toLowerCase();
// noinspection EqualityComparisonWithCoercionJS
return ua.match(/micromessenger/i) == 'micromessenger';
},
isReady(api) {
jweixin.ready(api);
},
// 初始化 JSSDK
async init(callback) {
if (!this.isWechat()) {
$helper.toast('请使用微信网页浏览器打开');
return;
}
// 调用后端接口,获得 JSSDK 初始化所需的签名
const url = location.origin;
const { code, data } = await AuthUtil.createWeixinMpJsapiSignature(url);
if (code === 0) {
jweixin.config({
debug: false,
appId: data.appId,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
jsApiList: [
'chooseWXPay',
'openLocation',
'getLocation',
'updateAppMessageShareData',
'updateTimelineShareData',
'scanQRCode',
], // TODO 芋艿:后续可以设置更多权限;
openTagList: data.openTagList,
});
} else {
console.log('请求 JSSDK 配置失败,错误码:', code);
}
// 监听结果
configSuccess = true;
jweixin.error((err) => {
configSuccess = false;
console.error('微信 JSSDK 初始化失败', err);
$helper.toast('微信JSSDK:' + err.errMsg);
});
jweixin.ready(() => {
if (configSuccess) {
console.log('微信 JSSDK 初始化成功');
}
});
// 回调
if (callback) {
callback(data);
}
},
//在需要定位页面调用 TODO 芋艿:未测试
getLocation(callback) {
this.isReady(() => {
jweixin.getLocation({
type: 'gcj02', // 默认为wgs84的gps坐标如果要返回直接给openLocation用的火星坐标可传入'gcj02'
success: function (res) {
callback(res);
},
fail: function (res) {
console.log('%c微信H5sdk,getLocation失败', 'color:green;background:yellow');
},
});
});
},
// 获取微信收货地址
openAddress(callback) {
this.isReady(() => {
jweixin.openAddress({
success: function (res) {
callback.success && callback.success(res);
},
fail: function (err) {
callback.error && callback.error(err);
console.log('%c微信H5sdk,openAddress失败', 'color:green;background:yellow');
},
complete: function (res) {},
});
});
},
// 微信扫码 TODO 芋艿:未测试
scanQRCode(callback) {
this.isReady(() => {
jweixin.scanQRCode({
needResult: 1, // 默认为0扫描结果由微信处理1则直接返回扫描结果
scanType: ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
callback(res);
},
fail: function (res) {
console.log('%c微信H5sdk,scanQRCode失败', 'color:green;background:yellow');
},
});
});
},
// 更新微信分享信息
updateShareInfo(data, callback = null) {
this.isReady(() => {
const shareData = {
title: data.title,
desc: data.desc,
link: data.link,
imgUrl: data.image,
success: function (res) {
if (callback) {
callback(res);
}
// 分享后的一些操作,比如分享统计等等
},
cancel: function (res) {},
};
// 新版 分享聊天api
jweixin.updateAppMessageShareData(shareData);
// 新版 分享到朋友圈api
jweixin.updateTimelineShareData(shareData);
});
},
// 打开坐标位置 TODO 芋艿:未测试
openLocation(data, callback) {
this.isReady(() => {
jweixin.openLocation({
...data,
success: function (res) {
console.log(res);
},
});
});
},
// 选择图片 TODO 芋艿:未测试
chooseImage(callback) {
this.isReady(() => {
jweixin.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album'],
success: function (rs) {
callback(rs);
},
});
});
},
// 微信支付
wxpay(data, callback) {
this.isReady(() => {
jweixin.chooseWXPay({
timestamp: data.timeStamp, // 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: data.nonceStr, // 支付签名随机串,不长于 32 位
package: data.packageValue, // 统一支付接口返回的prepay_id参数值提交格式如prepay_id=\*\*\*
signType: data.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: data.paySign, // 支付签名
success: function (res) {
callback.success && callback.success(res);
},
fail: function (err) {
callback.fail && callback.fail(err);
},
cancel: function (err) {
callback.cancel && callback.cancel(err);
},
});
});
},
};

168
sheep/platform/index.js Normal file
View File

@@ -0,0 +1,168 @@
/**
* Shopro 第三方平台功能聚合
* @version 1.0.3
* @author lidongtony
* @param {String} name - 厂商+平台名称
* @param {String} provider - 厂商
* @param {String} platform - 平台名称
* @param {String} os - 系统型号
* @param {Object} device - 设备信息
*/
import { isEmpty } from 'lodash-es';
// #ifdef H5
import { isWxBrowser } from '@/sheep/helper/utils';
// #endif
import wechat from './provider/wechat/index.js';
import apple from './provider/apple';
import share from './share';
const device = uni.getWindowInfo();
const os = uni.getDeviceInfo().platform;
let name = '';
let provider = '';
let platform = '';
let isWechatInstalled = true;
// #ifdef H5
if (isWxBrowser()) {
name = 'WechatOfficialAccount';
provider = 'wechat';
platform = 'officialAccount';
} else {
name = 'H5';
platform = 'h5';
}
// #endif
// #ifdef APP-PLUS
name = 'App';
platform = 'openPlatform';
// 检查微信客户端是否安装否则AppleStore会因此拒绝上架
if (os === 'ios') {
isWechatInstalled = plus.ios.import('WXApi').isWXAppInstalled();
}
// #endif
// #ifdef MP-WEIXIN
name = 'WechatMiniProgram';
platform = 'miniProgram';
provider = 'wechat';
// #endif
if (isEmpty(name)) {
uni.showToast({
title: '暂不支持该平台',
icon: 'none',
});
}
// 加载当前平台前置行为
const load = () => {
if (provider === 'wechat') {
wechat.load();
}
};
// 使用厂商独占sdk name = 'wechat' | 'alipay' | 'apple'
const useProvider = (_provider = '') => {
if (_provider === '') _provider = provider;
if (_provider === 'wechat') return wechat;
if (_provider === 'apple') return apple;
};
/**
* 检查更新 (只检查小程序和App)
* @param {Boolean} silence - 静默检查
*/
const checkUpdate = (silence = false) => {
let canUpdate;
// #ifdef MP-WEIXIN
useProvider().checkUpdate(silence);
// #endif
// #ifdef APP-PLUS
// TODO: 热更新
// #endif
};
/**
* 检查网络
* @param {Boolean} silence - 静默检查
*/
async function checkNetwork() {
const networkStatus = await uni.getNetworkType();
if (networkStatus.networkType == 'none') {
return Promise.resolve(false);
}
return Promise.resolve(true);
}
// 获取小程序胶囊信息
const getCapsule = () => {
// #ifdef MP
let capsule = uni.getMenuButtonBoundingClientRect();
if (!capsule) {
capsule = {
bottom: 56,
height: 32,
left: 278,
right: 365,
top: 24,
width: 87,
};
}
return capsule;
// #endif
// #ifndef MP
return {
bottom: 56,
height: 32,
left: 278,
right: 365,
top: 24,
width: 87,
};
// #endif
};
const capsule = getCapsule();
// 标题栏高度
const getNavBar = () => {
return device.statusBarHeight + 44;
};
const navbar = getNavBar();
function getLandingPage() {
let page = '';
// #ifdef H5
page = location.href.split('?')[0];
// #endif
return page;
}
// 设置ios+公众号网页落地页 解决微信sdk签名问题
const landingPage = getLandingPage();
const _platform = {
name,
device,
os,
provider,
platform,
useProvider,
checkUpdate,
checkNetwork,
share,
load,
capsule,
navbar,
landingPage,
isWechatInstalled,
};
export default _platform;

View File

@@ -0,0 +1,36 @@
// import third from '@/sheep/api/third';
// TODO 芋艿:等后面搞 App 再弄
const login = () => {
return new Promise(async (resolve, reject) => {
const loginRes = await uni.login({
provider: 'apple',
success: () => {
uni.getUserInfo({
provider: 'apple',
success: async (res) => {
if (res.errMsg === 'getUserInfo:ok') {
const payload = res.userInfo;
const { error } = await third.apple.login({
payload,
shareInfo: uni.getStorageSync('shareLog') || {},
});
if (error === 0) {
resolve(true);
} else {
resolve(false);
}
}
},
});
},
fail: (err) => {
resolve(false);
},
});
});
};
export default {
login,
};

View File

@@ -0,0 +1,9 @@
// #ifdef APP-PLUS
import service from './app';
// #endif
let apple = {};
if (typeof service !== 'undefined') {
apple = service;
}
export default apple;

View File

@@ -0,0 +1,15 @@
// #ifdef H5
import service from './officialAccount';
// #endif
// #ifdef MP-WEIXIN
import service from './miniProgram';
// #endif
// #ifdef APP-PLUS
import service from './openPlatform';
// #endif
const wechat = service;
export default wechat;

View File

@@ -0,0 +1,241 @@
import AuthUtil from '@/sheep/api/system/auth';
import SocialApi from '@/sheep/api/system/social';
import UserApi from '@/sheep/api/system/user';
import sheep from '@/sheep';
const socialType = 34; // 社交类型 - 微信小程序
let subscribeEventList = [];
// 加载微信小程序
function load() {
checkUpdate();
getSubscribeTemplate();
}
// 微信小程序静默授权登陆
const login = async () => {
return new Promise(async (resolve, reject) => {
// 1. 获得微信 code
const codeResult = await uni.login();
if (codeResult.errMsg !== 'login:ok') {
return resolve(false);
}
// 2. 社交登录
const loginResult = await AuthUtil.socialLogin(socialType, codeResult.code, 'default');
if (loginResult.code === 0) {
setOpenid(loginResult.data.openid);
return resolve(true);
} else {
return resolve(false);
}
});
};
// 微信小程序手机号授权登陆
const mobileLogin = async (e) => {
return new Promise(async (resolve, reject) => {
if (e.errMsg !== 'getPhoneNumber:ok') {
return resolve(false);
}
// 1. 获得微信 code
const codeResult = await uni.login();
if (codeResult.errMsg !== 'login:ok') {
return resolve(false);
}
// 2. 一键登录
const loginResult = await AuthUtil.weixinMiniAppLogin(e.code, codeResult.code, 'default');
if (loginResult.code === 0) {
setOpenid(loginResult.data.openid);
return resolve(true);
} else {
return resolve(false);
}
});
};
// 微信小程序绑定
const bind = () => {
return new Promise(async (resolve, reject) => {
// 1. 获得微信 code
const codeResult = await uni.login();
if (codeResult.errMsg !== 'login:ok') {
return resolve(false);
}
// 2. 绑定账号
const bindResult = await SocialApi.socialBind(socialType, codeResult.code, 'default');
if (bindResult.code === 0) {
setOpenid(bindResult.data);
return resolve(true);
} else {
return resolve(false);
}
});
};
// 微信小程序解除绑定
const unbind = async (openid) => {
const { code } = await SocialApi.socialUnbind(socialType, openid);
return code === 0;
};
// 绑定用户手机号
const bindUserPhoneNumber = (e) => {
return new Promise(async (resolve, reject) => {
const { code } = await UserApi.updateUserMobileByWeixin(e.code);
if (code === 0) {
resolve(true);
}
resolve(false);
});
};
// 设置 openid 到本地存储,目前只有 pay 支付时会使用
function setOpenid(openid) {
uni.setStorageSync('openid', openid);
}
// 获得 openid
async function getOpenid(force = false) {
let openid = uni.getStorageSync('openid');
if (!openid && force) {
const info = await getInfo();
if (info && info.openid) {
openid = info.openid;
setOpenid(openid);
}
}
return openid;
}
// 获得社交信息
async function getInfo() {
const { code, data } = await SocialApi.getSocialUser(socialType);
if (code !== 0) {
return undefined;
}
return data;
}
// ========== 非登录相关的逻辑 ==========
// 小程序更新
const checkUpdate = async (silence = true) => {
if (uni.canIUse('getUpdateManager')) {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
if (res.hasUpdate) {
updateManager.onUpdateReady(function () {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success: function (res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(function () {
// 新的版本下载失败
// uni.showModal({
// title: '已经有新版本了哟~',
// content: '新版本已经上线啦,请您删除当前小程序,重新搜索打开~',
// });
});
} else {
if (!silence) {
uni.showModal({
title: '当前为最新版本',
showCancel: false,
});
}
}
});
}
};
// 获取订阅消息模板
async function getSubscribeTemplate() {
const { code, data } = await SocialApi.getSubscribeTemplateList();
if (code === 0) {
subscribeEventList = data;
}
}
// 订阅消息
function subscribeMessage(event, callback = undefined) {
let tmplIds = [];
if (typeof event === 'string') {
const temp = subscribeEventList.find((item) => item.title.includes(event));
if (temp) {
tmplIds.push(temp.id);
}
}
if (typeof event === 'object') {
event.forEach((e) => {
const temp = subscribeEventList.find((item) => item.title.includes(e));
if (temp) {
tmplIds.push(temp.id);
}
});
}
if (tmplIds.length === 0) return;
uni.requestSubscribeMessage({
tmplIds,
success: () => {
// 不管是拒绝还是同意都触发
callback && callback();
},
fail: (err) => {
console.log(err);
},
});
}
// 商家转账用户确认模式下,拉起页面请求用户确认收款 Transfer
function requestMerchantTransfer(mchId, packageInfo, successCallback, failCallback) {
if (!wx.canIUse('requestMerchantTransfer')) {
wx.showModal({
content: '你的微信版本过低,请更新至最新版本。',
showCancel: false,
});
return;
}
wx.requestMerchantTransfer({
mchId: mchId,
appId: wx.getAccountInfoSync().miniProgram.appId,
package: packageInfo,
success: (res) => {
// res.err_msg 将在页面展示成功后返回应用时返回 ok并不代表付款成功
console.log('success:', res);
successCallback && successCallback(res);
},
fail: (res) => {
console.log('fail:', res);
sheep.$helper.toast(res.errMsg);
failCallback && failCallback(res);
},
});
}
export default {
load,
login,
bind,
unbind,
bindUserPhoneNumber,
mobileLogin,
getInfo,
getOpenid,
subscribeMessage,
checkUpdate,
requestMerchantTransfer,
};

View File

@@ -0,0 +1,105 @@
import $wxsdk from '@/sheep/libs/sdk-h5-weixin';
import { getRootUrl } from '@/sheep/helper';
import AuthUtil from '@/sheep/api/system/auth';
import SocialApi from '@/sheep/api/system/social';
const socialType = 31; // 社交类型 - 微信公众号
// 加载微信公众号JSSDK
async function load() {
$wxsdk.init();
}
// 微信公众号登陆
async function login(code = '', state = '') {
// 情况一:没有 code 时,去获取 code
if (!code) {
const loginUrl = await getLoginUrl();
if (loginUrl) {
uni.setStorageSync('returnUrl', location.href);
window.location = loginUrl;
}
// 情况二:有 code 时,使用 code 去自动登录
} else {
// 解密 code 发起登陆
const loginResult = await AuthUtil.socialLogin(socialType, code, state);
if (loginResult.code === 0) {
setOpenid(loginResult.data.openid);
return loginResult;
}
}
return false;
}
// 微信公众号绑定
async function bind(code = '', state = '') {
// 情况一:没有 code 时,去获取 code
if (code === '') {
const loginUrl = await getLoginUrl('bind');
if (loginUrl) {
uni.setStorageSync('returnUrl', location.href);
window.location = loginUrl;
}
} else {
// 情况二:有 code 时,使用 code 去自动绑定
const loginResult = await SocialApi.socialBind(socialType, code, state);
if (loginResult.code === 0) {
setOpenid(loginResult.data);
return loginResult;
}
}
return false;
}
// 微信公众号解除绑定
const unbind = async (openid) => {
const { code } = await SocialApi.socialUnbind(socialType, openid);
return code === 0;
};
// 获取公众号登陆地址
async function getLoginUrl(event = 'login') {
const page = getRootUrl() + 'pages/index/login' + '?event=' + event; // event 目的,区分是 login 还是 bind
const { code, data } = await AuthUtil.socialAuthRedirect(socialType, page);
if (code !== 0) {
return undefined;
}
return data;
}
// 设置 openid 到本地存储,目前只有 pay 支付时会使用
function setOpenid(openid) {
uni.setStorageSync('openid', openid);
}
// 获得 openid
async function getOpenid(force = false) {
let openid = uni.getStorageSync('openid');
if (!openid && force) {
const info = await getInfo();
if (info && info.openid) {
openid = info.openid;
setOpenid(openid);
}
}
return openid;
}
// 获得社交信息
async function getInfo() {
const { code, data } = await SocialApi.getSocialUser(socialType);
if (code !== 0) {
return undefined;
}
return data;
}
export default {
load,
login,
bind,
unbind,
getInfo,
getOpenid,
jsWxSdk: $wxsdk,
};

View File

@@ -0,0 +1,64 @@
// 登录
import third from '@/sheep/api/migration/third';
import SocialApi from '@/sheep/api/system/social';
import $share from '@/sheep/platform/share';
// TODO 芋艿:等后面搞 App 再弄
const socialType = 32; // 社交类型 - 微信开放平台
const load = async () => {};
// 微信开放平台移动应用授权登陆
const login = () => {
return new Promise(async (resolve, reject) => {
const loginRes = await uni.login({
provider: 'weixin',
onlyAuthorize: true,
});
debugger
if (loginRes.errMsg == 'login:ok') {
// TODO third.wechat.login 函数未实现
const res = await third.wechat.login({
platform: 'openPlatform',
shareInfo: uni.getStorageSync('shareLog') || {},
payload: encodeURIComponent(
JSON.stringify({
code: loginRes.code,
}),
),
});
if (res.error === 0) {
$share.bindBrokerageUser()
resolve(true);
}
} else {
uni.showToast({
icon: 'none',
title: loginRes.errMsg,
});
}
resolve(false);
});
};
// 微信 App 解除绑定
const unbind = async (openid) => {
const { code } = await SocialApi.socialUnbind(socialType, openid);
return code === 0;
};
// 获得社交信息
async function getInfo() {
const { code, data } = await SocialApi.getSocialUser(socialType);
if (code !== 0) {
return undefined;
}
return data;
}
export default {
load,
login,
getInfo
};

184
sheep/platform/share.js Normal file
View File

@@ -0,0 +1,184 @@
import $store from '@/sheep/store';
import $platform from '@/sheep/platform';
import $router from '@/sheep/router';
import $url from '@/sheep/url';
import { SharePageEnum } from '@/sheep/helper/const';
// #ifdef H5
import $wxsdk from '@/sheep/libs/sdk-h5-weixin';
// #endif
// 设置分享的平台渠道: 1=H5,2=微信公众号网页,3=微信小程序,4=App,...按需扩展
const platformMap = ['H5', 'WechatOfficialAccount', 'WechatMiniProgram', 'App'];
// 设置分享方式: 1=直接转发,2=海报,3=复制链接,...按需扩展
const fromMap = ['forward', 'poster', 'link'];
// 设置分享信息参数
const getShareInfo = (
scene = {
title: '', // 自定义分享标题
desc: '', // 自定义描述
image: '', // 自定义分享图片
params: {}, // 自定义分享参数
},
poster = {
// 自定义海报数据
type: 'user',
},
) => {
const shareInfo = {
title: '', // 分享标题
desc: '', // 描述
image: '', // 分享图片
path: '', // 分享页面+参数
link: '', // 分享Url+参数
query: '', // 分享参数
poster, // 海报所需数据
forward: {}, // 转发所需参数
};
shareInfo.title = scene.title;
shareInfo.image = $url.cdn(scene.image);
shareInfo.desc = scene.desc;
const app = $store('app');
const shareConfig = app.platform.share;
// 自动拼接分享用户参数
const query = buildSpmQuery(scene.params);
shareInfo.query = query;
// 配置分享链接地址
shareInfo.link = buildSpmLink(query, shareConfig.linkAddress);
// 配置页面地址带参数
shareInfo.path = buildSpmPath();
// 配置页面转发参数
if (shareConfig.methods.includes('forward')) {
shareInfo.forward.path = buildSpmPath(query);
}
return shareInfo;
};
/**
* 构造 spm 分享参数
*
* @param params json 格式其中包含1shareId 分享用户的编号2page 页面类型3query 页面 ID参数4platform 平台类型5from 分享来源类型。
* @return 分享串 `spm=${shareId}.${page}.${query}.${platform}.${from}`
*/
const buildSpmQuery = (params) => {
const user = $store('user');
let shareId = '0'; // 设置分享者用户ID
if (typeof params.shareId === 'undefined') {
if (user.isLogin) {
shareId = user.userInfo.id;
}
}
let page = SharePageEnum.HOME.value; // 页面类型,默认首页
if (typeof params.page !== 'undefined') {
page = params.page;
}
let query = '0'; // 设置页面ID: 如商品ID、拼团ID等
if (typeof params.query !== 'undefined') {
query = params.query;
}
let platform = platformMap.indexOf($platform.name) + 1;
let from = '1';
if (typeof params.from !== 'undefined') {
from = platformMap.indexOf(params.from) + 1;
}
// spmParams = ... 可按需扩展
return `spm=${shareId}.${page}.${query}.${platform}.${from}`;
};
// 构造页面分享参数: 所有的分享都先到首页进行 spm 参数解析
const buildSpmPath = (query) => {
// 默认是主页,页面 page例如 pages/index/index根路径前不要填加 /
// 不能携带参数参数请放在scene字段里如果不填写这个字段默认跳主页面。scancode_time为系统保留参数不允许配置
// 页面分享时参数使用 ? 拼接
return typeof query === 'undefined' ? `pages/index/index` : `pages/index/index?${query}`;
};
// 构造分享链接
const buildSpmLink = (query, linkAddress = '') => {
return `${linkAddress}?${query}`;
};
// 解析Spm
const decryptSpm = (spm) => {
const user = $store('user');
let shareParamsArray = spm.split('.');
let shareParams = {
spm,
shareId: 0,
page: '',
query: {},
platform: '',
from: '',
};
let query;
shareParams.shareId = shareParamsArray[0];
switch (shareParamsArray[1]) {
case SharePageEnum.HOME.value:
// 默认首页不跳转
shareParams.page = SharePageEnum.HOME.page;
break;
case SharePageEnum.GOODS.value:
// 普通商品
shareParams.page = SharePageEnum.GOODS.page;
shareParams.query = {
id: shareParamsArray[2], // 设置活动编号
};
break;
}
shareParams.platform = platformMap[shareParamsArray[3] - 1];
shareParams.from = fromMap[shareParamsArray[4] - 1];
if (shareParams.shareId !== 0) {
// 记录分享者编号
uni.setStorageSync('shareId', shareParams.shareId);
// 已登录 绑定推广员
if (!!user.isLogin) {
bindBrokerageUser(shareParams.shareId);
}
}
if (shareParams.page !== SharePageEnum.HOME.page) {
$router.go(shareParams.page, shareParams.query);
}
return shareParams;
};
// 绑定推广员
const bindBrokerageUser = async (val = undefined) => {
try {
const shareId = val || uni.getStorageSync('shareId');
if (!shareId) {
return;
}
// 绑定成功返回 true失败返回 false
const { data } = await BrokerageApi.bindBrokerageUser({ bindUserId: shareId });
// 绑定成功后清除缓存
if (data) {
uni.removeStorageSync('shareId');
}
} catch (e) {
console.error(e);
}
};
// 更新公众号分享sdk
const updateShareInfo = (shareInfo) => {
// #ifdef H5
if ($platform.name === 'WechatOfficialAccount') {
$wxsdk.updateShareInfo(shareInfo);
}
// #endif
};
export default {
getShareInfo,
updateShareInfo,
decryptSpm,
bindBrokerageUser,
};

493
sheep/request/index.js Normal file
View File

@@ -0,0 +1,493 @@
/**
* Shopro-request
* @description api模块管理loading配置请求拦截错误处理
*/
import Request from 'luch-request';
import { apiPath, baseUrl, tenantId } from '@/sheep/config';
import $store from '@/sheep/store';
import $platform from '@/sheep/platform';
import AuthUtil from '@/sheep/api/system/auth';
import { getTerminal } from '@/sheep/helper/const';
const options = {
// 显示操作成功消息 默认不显示
showSuccess: false,
// 成功提醒 默认使用后端返回值
successMsg: '',
// 显示失败消息 默认显示
showError: true,
// 失败提醒 默认使用后端返回信息
errorMsg: '',
// 显示请求时loading模态框 默认显示
showLoading: true,
// loading提醒文字
loadingMsg: '加载中',
// 需要授权才能请求 默认放开
auth: false,
// 是否传递 token
isToken: true,
};
const COMPANY_DEPT_RETRY_HEADER = '__companyDeptRetried';
const VISIT_COMPANY_STORAGE_KEY = 'visit-company-info';
const VISIT_DEPT_STORAGE_KEY = 'visit-dept-info';
// Loading全局实例
let LoadingInstance = {
target: null,
count: 0,
};
/**
* 关闭loading
*/
function closeLoading() {
if (LoadingInstance.count > 0) LoadingInstance.count--;
if (LoadingInstance.count === 0) uni.hideLoading();
}
/**
* @description 请求基础配置 可直接使用访问自定义请求
*/
const http = new Request({
baseURL: baseUrl + apiPath,
timeout: 8000,
method: 'GET',
header: {
Accept: 'text/json',
'Content-Type': 'application/json;charset=UTF-8',
platform: $platform.name,
},
// #ifdef APP-PLUS
sslVerify: false,
// #endif
// #ifdef H5
// 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+
withCredentials: false,
// #endif
custom: options,
});
/**
* @description 请求拦截器
*/
http.interceptors.request.use(
(config) => {
// 自定义处理【auth 授权】:必须登录的接口,则跳转到登录页
if (config.custom.auth && !$store('user').isLogin) {
uni.navigateTo({
url: '/pages/login/index'
});
return Promise.reject();
}
// 自定义处理【loading 加载中】:如果需要显示 loading则显示 loading
if (config.custom.showLoading) {
LoadingInstance.count++;
LoadingInstance.count === 1 &&
uni.showLoading({
title: config.custom.loadingMsg,
mask: true,
fail: () => {
uni.hideLoading();
},
});
}
// 增加 token 令牌、terminal 终端、tenant 租户的请求头
// 检查是否在白名单中白名单中的接口不需要token
let isToken = config.custom.isToken;
whiteList.some((v) => {
if (config.url && config.url.indexOf(v) > -1) {
return (isToken = false);
}
});
const token = isToken ? getAccessToken() : undefined;
if (token) {
config.header['Authorization'] = token;
}
config.header['terminal'] = getTerminal();
config.header['Accept'] = '*/*';
config.header['tenant-id'] = getTenantId();
const visitCompanyId = getVisitCompanyId();
if (visitCompanyId !== undefined && visitCompanyId !== null && visitCompanyId !== '') {
config.header['visit-company-id'] = visitCompanyId;
const visitCompanyName = getVisitCompanyName();
if (visitCompanyName !== undefined && visitCompanyName !== null) {
config.header['visit-company-name'] = encodeURIComponent(visitCompanyName || '');
}
}
const visitDeptId = getVisitDeptId();
if (visitDeptId !== undefined && visitDeptId !== null && visitDeptId !== '') {
config.header['visit-dept-id'] = visitDeptId;
const visitDeptName = getVisitDeptName();
if (visitDeptName !== undefined && visitDeptName !== null) {
config.header['visit-dept-name'] = encodeURIComponent(visitDeptName || '');
}
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
/**
* @description 响应拦截器
*/
http.interceptors.response.use(
(response) => {
// 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌
if (response.config.url.indexOf('/system/auth/') >= 0 && response.data?.data?.accessToken) {
$store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken);
}
// 自定处理【loading 加载中】:如果需要显示 loading则关闭 loading
response.config.custom.showLoading && closeLoading();
// 统一处理【公司/部门二次选择】:参考 PC 端逻辑,自动补全或提示选择后重试
if (response.data.code === 400 && Array.isArray(response.data.data)) {
const companyDeptList = response.data.data;
const config = response.config;
config.header = config.header || {};
if (companyDeptList.length === 1) {
const item = companyDeptList[0];
if (!config.header[COMPANY_DEPT_RETRY_HEADER]) {
applyCompanyDeptSelection(item, config);
return request(config);
}
uni.showToast({
title: '公司/部门信息缺失,且自动补全失败,请联系管理员',
icon: 'none',
mask: true,
});
return Promise.resolve(response.data);
} else if (companyDeptList.length > 1) {
const groupedList = normalizeCompanyDeptList(companyDeptList);
const companyDeptDialogStore = $store('company-dept');
return new Promise((resolve) => {
companyDeptDialogStore.open({
companyList: groupedList,
onConfirm: ({ companyId, deptId }) => {
const selectedCompany = groupedList.find((company) => company.companyId === companyId);
const selectedDept = selectedCompany?.depts.find((dept) => dept.deptId === deptId);
applyCompanyDeptSelection(
{
companyId,
companyName: selectedCompany?.companyName || '',
deptId,
deptName: selectedDept?.deptName || '',
},
config,
);
resolve(request(config));
},
onCancel: () => {
uni.showToast({
title: '已取消公司/部门选择',
icon: 'none',
mask: true,
});
resolve(response.data);
},
});
});
}
}
// 自定义处理【error 错误提示】:如果需要显示错误提示,则显示错误提示
if (response.data.code !== 0 && response.data.repCode !== '0000') {
// 特殊:如果 401 错误码,则跳转到登录页 or 刷新令牌
if (response.data.code === 401) {
return refreshToken(response.config);
}
// 特殊:处理分销用户绑定失败的提示
if ((response.data.code + '').includes('1011007')) {
console.error(`分销用户绑定失败,原因:${response.data.msg}`);
} else if (response.config.custom.showError) {
// 错误提示
uni.showToast({
title: response.data.msg || '服务器开小差啦,请稍后再试~',
icon: 'none',
mask: true,
});
}
// 即使是错误响应,也应该返回完整的响应数据,让调用方自行处理
return Promise.resolve(response.data);
}
// 自定义处理【showSuccess 成功提示】:如果需要显示成功提示,则显示成功提示
if (
response.config.custom.showSuccess &&
response.config.custom.successMsg !== '' &&
response.data.code === 0
) {
uni.showToast({
title: response.config.custom.successMsg,
icon: 'none',
});
}
// 返回结果:包括 code + data + msg
return Promise.resolve(response.data);
},
(error) => {
const userStore = $store('user');
const isLogin = userStore.isLogin;
let errorMessage = '网络请求出错';
if (error !== undefined) {
switch (error.statusCode) {
case 400:
errorMessage = '请求错误';
break;
case 401:
errorMessage = isLogin ? '您的登陆已过期' : '请先登录';
// 正常情况下,后端不会返回 401 错误,所以这里不处理 handleAuthorized
break;
case 403:
errorMessage = '拒绝访问';
break;
case 404:
errorMessage = '请求出错';
break;
case 408:
errorMessage = '请求超时';
break;
case 429:
errorMessage = '请求频繁, 请稍后再访问';
break;
case 500:
errorMessage = '服务器开小差啦,请稍后再试~';
break;
case 501:
errorMessage = '服务未实现';
break;
case 502:
errorMessage = '网络错误';
break;
case 503:
errorMessage = '服务不可用';
break;
case 504:
errorMessage = '网络超时';
break;
case 505:
errorMessage = 'HTTP 版本不受支持';
break;
}
if (error.errMsg.includes('timeout')) errorMessage = '请求超时';
// #ifdef H5
if (error.errMsg.includes('Network'))
errorMessage = window.navigator.onLine ? '服务器异常' : '请检查您的网络连接';
// #endif
}
if (error && error.config) {
if (error.config.custom.showError) {
uni.showToast({
title: error.data?.msg || errorMessage,
icon: 'none',
mask: true,
});
}
error.config.custom.showLoading && closeLoading();
}
return false;
},
);
// 请求白名单,无须 token 的接口
const whiteList = ['/login', '/refresh-token', '/captcha/get', '/system/captcha'];
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
let requestList = []; // 请求队列
let isRefreshToken = false; // 是否正在刷新中
const refreshToken = async (config) => {
// 如果当前已经是 refresh-token 的 URL 地址,并且还是 401 错误,说明是刷新令牌失败了,直接返回 Promise.reject(error)
if (config.url.indexOf('/system/auth/refresh-token') >= 0) {
return Promise.reject('error');
}
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true;
// 1. 如果获取不到刷新令牌,则只能执行登出操作
const refreshToken = getRefreshToken();
if (!refreshToken) {
return handleAuthorized();
}
// 2. 进行刷新访问令牌
try {
const refreshTokenResult = await AuthUtil.refreshToken(refreshToken);
if (refreshTokenResult.code !== 0) {
// 如果刷新不成功,直接抛出 e 触发 2.2 的逻辑
// noinspection ExceptionCaughtLocallyJS
throw new Error('刷新令牌失败');
}
// 2.1 刷新成功,则回放队列的请求 + 当前请求
config.header.Authorization = 'Bearer ' + getAccessToken();
requestList.forEach((cb) => {
cb();
});
requestList = [];
return request(config);
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb) => {
cb();
});
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized();
} finally {
requestList = [];
isRefreshToken = false;
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
config.header.Authorization = 'Bearer ' + getAccessToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(request(config));
});
});
}
};
/**
* 处理 401 未登录的错误
*/
const handleAuthorized = () => {
const userStore = $store('user');
userStore.logout(true);
uni.navigateTo({
url: '/pages/login/index'
});
// 登录超时
return Promise.reject({
code: 401,
msg: userStore.isLogin ? '您的登陆已过期' : '请先登录',
});
};
/** 获得访问令牌 */
export const getAccessToken = () => {
return uni.getStorageSync('token');
};
/** 获得刷新令牌 */
export const getRefreshToken = () => {
return uni.getStorageSync('refresh-token');
};
/** 获得租户编号 */
export const getTenantId = () => {
return uni.getStorageSync('tenant-id') || tenantId;
};
const getStorageObject = (key) => {
const value = uni.getStorageSync(key);
if (!value) {
return {};
}
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (error) {
console.warn(`解析本地存储 ${key} 失败:`, error);
return {};
}
}
return value;
};
const setStorageObject = (key, value) => {
if (value === undefined || value === null) {
uni.removeStorageSync(key);
return;
}
uni.setStorageSync(key, value);
};
export const getVisitCompanyId = () => {
const info = getStorageObject(VISIT_COMPANY_STORAGE_KEY);
return info?.id ?? info?.companyId ?? null;
};
export const getVisitCompanyName = () => {
const info = getStorageObject(VISIT_COMPANY_STORAGE_KEY);
return info?.name ?? info?.companyName ?? '';
};
export const getVisitDeptId = () => {
const info = getStorageObject(VISIT_DEPT_STORAGE_KEY);
return info?.id ?? info?.deptId ?? null;
};
export const getVisitDeptName = () => {
const info = getStorageObject(VISIT_DEPT_STORAGE_KEY);
return info?.name ?? info?.deptName ?? '';
};
export const setVisitCompany = (companyId, companyName) => {
if (companyId === undefined || companyId === null || companyId === '') {
uni.removeStorageSync(VISIT_COMPANY_STORAGE_KEY);
return;
}
setStorageObject(VISIT_COMPANY_STORAGE_KEY, {
id: companyId,
name: companyName || '',
});
};
export const setVisitDept = (deptId, deptName) => {
if (deptId === undefined || deptId === null || deptId === '') {
uni.removeStorageSync(VISIT_DEPT_STORAGE_KEY);
return;
}
setStorageObject(VISIT_DEPT_STORAGE_KEY, {
id: deptId,
name: deptName || '',
});
};
const applyCompanyDeptSelection = (item, config) => {
setVisitCompany(item.companyId, item.companyName);
setVisitDept(item.deptId, item.deptName);
config.header['visit-company-id'] = item.companyId;
config.header['visit-company-name'] = encodeURIComponent(item.companyName || '');
config.header['visit-dept-id'] = item.deptId;
config.header['visit-dept-name'] = encodeURIComponent(item.deptName || '');
config.header[COMPANY_DEPT_RETRY_HEADER] = '1';
};
const normalizeCompanyDeptList = (list = []) => {
const companyMap = new Map();
list.forEach((item) => {
if (!companyMap.has(item.companyId)) {
companyMap.set(item.companyId, {
companyId: item.companyId,
companyName: item.companyName,
depts: [],
});
}
const company = companyMap.get(item.companyId);
if (!company.depts.some((dept) => dept.deptId === item.deptId)) {
company.depts.push({
deptId: item.deptId,
deptName: item.deptName,
});
}
});
return Array.from(companyMap.values());
};
const request = (config) => {
return http.middleware(config);
};
export default request;

185
sheep/router/index.js Normal file
View File

@@ -0,0 +1,185 @@
import $store from '@/sheep/store';
import { showShareModal } from '@/sheep/hooks/useModal';
import { isNumber, isString, isEmpty, startsWith, isObject, isNil, clone } from 'lodash-es';
import throttle from '@/sheep/helper/throttle';
const _go = (
path,
params = {},
options = {
redirect: false,
},
) => {
let page = ''; // 跳转页面
let query = ''; // 页面参数
let url = ''; // 跳转页面完整路径
if (isString(path)) {
// 判断跳转类型是 path 还是http
if (startsWith(path, 'http')) {
// #ifdef H5
window.location = path;
return;
// #endif
// #ifndef H5
page = `/pages/public/webview`;
query = `url=${encodeURIComponent(path)}`;
// #endif
} else if (startsWith(path, 'action:')) {
handleAction(path);
return;
} else {
[page, query] = path.split('?');
}
if (!isEmpty(params)) {
let query2 = paramsToQuery(params);
if (isEmpty(query)) {
query = query2;
} else {
query += '&' + query2;
}
}
}
if (isObject(path)) {
page = path.url;
if (!isNil(path.params)) {
query = paramsToQuery(path.params);
}
}
const nextRoute = ROUTES_MAP[page];
// 未找到指定跳转页面
// mark: 跳转404页
if (!nextRoute) {
console.log(`%c跳转路径参数错误<${page || 'EMPTY'}>`, 'color:red;background:yellow');
return;
}
// 页面登录拦截
if (nextRoute.meta?.auth && !$store('user').isLogin) {
go('/pages/login/index');
return;
}
url = page;
if (!isEmpty(query)) {
url += `?${query}`;
}
// 跳转底部导航
if (TABBAR.includes(page)) {
uni.switchTab({
url,
});
return;
}
// 使用redirect跳转
if (options.redirect) {
uni.redirectTo({
url,
});
return;
}
uni.navigateTo({
url,
});
};
// 限流 防止重复点击跳转
function go(...args) {
throttle(() => {
_go(...args);
});
}
function paramsToQuery(params) {
if (isEmpty(params)) {
return '';
}
// return new URLSearchParams(Object.entries(params)).toString();
let query = [];
for (let key in params) {
query.push(key + '=' + params[key]);
}
return query.join('&');
}
function back() {
// #ifdef H5
history.back();
// #endif
// #ifndef H5
uni.navigateBack();
// #endif
}
function redirect(path, params = {}) {
go(path, params, {
redirect: true,
});
}
// 检测是否有浏览器历史
function hasHistory() {
// #ifndef H5
const pages = getCurrentPages();
if (pages.length > 1) {
return true;
}
return false;
// #endif
// #ifdef H5
return !!history.state.back;
// #endif
}
function getCurrentRoute(field = '') {
let currentPage = getCurrentPage();
// #ifdef MP
currentPage.$page['route'] = currentPage.route;
currentPage.$page['options'] = currentPage.options;
// #endif
if (field !== '') {
return currentPage.$page[field];
} else {
return currentPage.$page;
}
}
function getCurrentPage() {
let pages = getCurrentPages();
return pages[pages.length - 1];
}
function handleAction(path) {
const action = path.split(':');
switch (action[1]) {
case 'showShareModal':
showShareModal();
break;
}
}
function error(errCode, errMsg = '') {
redirect('/pages/public/error', {
errCode,
errMsg,
});
}
export default {
go,
back,
hasHistory,
redirect,
getCurrentPage,
getCurrentRoute,
error,
};

View File

@@ -0,0 +1,79 @@
const singleComment = Symbol('singleComment');
const multiComment = Symbol('multiComment');
const stripWithoutWhitespace = () => '';
const stripWithWhitespace = (string, start, end) => string.slice(start, end).replace(/\S/g, ' ');
const isEscaped = (jsonString, quotePosition) => {
let index = quotePosition - 1;
let backslashCount = 0;
while (jsonString[index] === '\\') {
index -= 1;
backslashCount += 1;
}
return Boolean(backslashCount % 2);
};
export default function stripJsonComments(jsonString, { whitespace = true } = {}) {
if (typeof jsonString !== 'string') {
throw new TypeError(
`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``,
);
}
const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace;
let isInsideString = false;
let isInsideComment = false;
let offset = 0;
let result = '';
for (let index = 0; index < jsonString.length; index++) {
const currentCharacter = jsonString[index];
const nextCharacter = jsonString[index + 1];
if (!isInsideComment && currentCharacter === '"') {
const escaped = isEscaped(jsonString, index);
if (!escaped) {
isInsideString = !isInsideString;
}
}
if (isInsideString) {
continue;
}
if (!isInsideComment && currentCharacter + nextCharacter === '//') {
result += jsonString.slice(offset, index);
offset = index;
isInsideComment = singleComment;
index++;
} else if (isInsideComment === singleComment && currentCharacter + nextCharacter === '\r\n') {
index++;
isInsideComment = false;
result += strip(jsonString, offset, index);
offset = index;
continue;
} else if (isInsideComment === singleComment && currentCharacter === '\n') {
isInsideComment = false;
result += strip(jsonString, offset, index);
offset = index;
} else if (!isInsideComment && currentCharacter + nextCharacter === '/*') {
result += jsonString.slice(offset, index);
offset = index;
isInsideComment = multiComment;
index++;
continue;
} else if (isInsideComment === multiComment && currentCharacter + nextCharacter === '*/') {
index++;
isInsideComment = false;
result += strip(jsonString, offset, index + 1);
offset = index + 1;
continue;
}
}
return result + (isInsideComment ? strip(jsonString.slice(offset)) : jsonString.slice(offset));
}

View File

@@ -0,0 +1,103 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true,
});
const fs = require('fs');
import stripJsonComments from './strip-json-comments';
import { isArray, isEmpty } from 'lodash';
class TransformPages {
constructor({ includes, pagesJsonDir }) {
this.includes = includes;
this.uniPagesJSON = JSON.parse(stripJsonComments(fs.readFileSync(pagesJsonDir, 'utf-8')));
this.routes = this.getPagesRoutes().concat(this.getSubPackagesRoutes());
this.tabbar = this.getTabbarRoutes();
this.routesMap = this.transformPathToKey(this.routes);
}
/**
* 通过读取pages.json文件 生成直接可用的routes
*/
getPagesRoutes(pages = this.uniPagesJSON.pages, rootPath = null) {
let routes = [];
for (let i = 0; i < pages.length; i++) {
const item = pages[i];
let route = {};
for (let j = 0; j < this.includes.length; j++) {
const key = this.includes[j];
let value = item[key];
if (key === 'path') {
value = rootPath ? `/${rootPath}/${value}` : `/${value}`;
}
if (key === 'aliasPath' && i == 0 && rootPath == null) {
route[key] = route[key] || '/';
} else if (value !== undefined) {
route[key] = value;
}
}
routes.push(route);
}
return routes;
}
/**
* 解析小程序分包路径
*/
getSubPackagesRoutes() {
if (!(this.uniPagesJSON && this.uniPagesJSON.subPackages)) {
return [];
}
const subPackages = this.uniPagesJSON.subPackages;
let routes = [];
for (let i = 0; i < subPackages.length; i++) {
const subPages = subPackages[i].pages;
const root = subPackages[i].root;
const subRoutes = this.getPagesRoutes(subPages, root);
routes = routes.concat(subRoutes);
}
return routes;
}
getTabbarRoutes() {
if (!(this.uniPagesJSON && this.uniPagesJSON.tabBar && this.uniPagesJSON.tabBar.list)) {
return [];
}
const tabbar = this.uniPagesJSON.tabBar.list;
let tabbarMap = [];
tabbar.forEach((bar) => {
tabbarMap.push('/' + bar.pagePath);
});
return tabbarMap;
}
transformPathToKey(list) {
if (!isArray(list) || isEmpty(list)) {
return [];
}
let map = {};
list.forEach((i) => {
map[i.path] = i;
});
return map;
}
}
function uniReadPagesV3Plugin({ pagesJsonDir, includes }) {
let defaultIncludes = ['path', 'aliasPath', 'name'];
includes = [...defaultIncludes, ...includes];
let pages = new TransformPages({
pagesJsonDir,
includes,
});
return {
name: 'uni-read-pages-v3',
config(config) {
return {
define: {
ROUTES: pages.routes,
ROUTES_MAP: pages.routesMap,
TABBAR: pages.tabbar,
},
};
},
};
}
exports.default = uniReadPagesV3Plugin;

354
sheep/scss/_main.scss Normal file
View File

@@ -0,0 +1,354 @@
body {
color: var(--text-a);
background-color: var(--ui-BG-1) !important;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
/* ==================
初始化
==================== */
.ui-link {
cursor: pointer;
}
navigator {
display: inline-flex;
}
navigator.navigator-hover {
background-color: inherit;
transform: translate(1rpx, 1rpx);
// opacity: 1;
}
/* ==================
辅助类
==================== */
.none {
display: none !important;
}
.inline {
display: inline !important;
}
.inline-block {
display: inline-block !important;
}
.block {
display: block !important;
}
.touch-none {
pointer-events: none;
}
.touch-all {
pointer-events: all;
}
.flex {
display: flex !important;
}
.inline-flex {
display: inline-flex !important;
}
.w-100 {
width: 100%;
}
/* -- 浮动 -- */
.cf::after,
.cf::before {
content: '';
display: table;
}
.cf::after {
clear: both;
}
.fl {
float: left;
}
.fr {
float: right;
}
.position-center {
@include position-center;
}
.position-relative {
position: relative;
}
/* -- 工具类 -- */
@function negativify-map($map) {
$result: ();
@each $key, $value in $map {
@if $key != 0 {
$result: map-merge($result, ('n' + $key: (-$value)));
}
}
@return $result;
}
$utilities: () !default;
$utilities: map-merge(
(
'margin': (
responsive: true,
property: margin,
class: m,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-x': (
property: margin-right margin-left,
class: mx,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-y': (
property: margin-top margin-bottom,
class: my,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-top': (
property: margin-top,
class: mt,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-right': (
property: margin-right,
class: mr,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-bottom': (
property: margin-bottom,
class: mb,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-left': (
property: margin-left,
class: ml,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'padding': (
responsive: true,
property: padding,
class: p,
values: $spacers,
),
'padding-x': (
property: padding-right padding-left,
class: px,
values: $spacers,
),
'padding-y': (
property: padding-top padding-bottom,
class: py,
values: $spacers,
),
'padding-top': (
property: padding-top,
class: pt,
values: $spacers,
),
'padding-right': (
property: padding-right,
class: pr,
values: $spacers,
),
'padding-bottom': (
property: padding-bottom,
class: pb,
values: $spacers,
),
'padding-left': (
property: padding-left,
class: pl,
values: $spacers,
),
'font-weight': (
property: font-weight,
class: text,
values: (
light: $font-weight-light,
lighter: $font-weight-lighter,
normal: $font-weight-normal,
bold: $font-weight-bold,
bolder: $font-weight-bolder,
),
),
'text-align': (
property: text-align,
class: text,
values: left right center,
),
'font-color': (
property: color,
class: text,
values:
map-merge(
$colors,
map-merge(
$grays,
map-merge(
$darks,
(
'reset': inherit,
)
)
)
),
),
'line-height': (
property: line-height,
class: lh,
values: (
1: 1,
sm: $line-height-sm,
base: $line-height-base,
lg: $line-height-lg,
),
),
'white-space': (
property: white-space,
class: text,
values: (
nowrap: nowrap,
),
),
'radius': (
property: border-radius,
class: radius,
values: (
null: $radius,
sm: $radius-sm,
lg: $radius-lg,
0: 0,
),
),
'round': (
property: border-radius,
class: round,
values: (
null: $round-pill,
circle: 50%,
),
),
'radius-top': (
property: border-top-left-radius border-top-right-radius,
class: radius-top,
values: (
null: $radius,
),
),
'radius-right': (
property: border-top-right-radius border-bottom-right-radius,
class: radius-right,
values: (
null: $radius,
),
),
'radius-bottom': (
property: border-bottom-right-radius border-bottom-left-radius,
class: radius-bottom,
values: (
null: $radius,
),
),
'radius-left': (
property: border-bottom-left-radius border-top-left-radius,
class: radius-left,
values: (
null: $radius,
),
),
'radius-lr': (
property: border-top-left-radius border-bottom-right-radius,
class: radius-lr,
values: (
null: $radius,
),
),
'radius-lrs': (
property: border-top-right-radius border-bottom-left-radius,
class: radius-lr,
values: (
null: 0,
),
),
'radius-rl': (
property: border-top-right-radius border-bottom-left-radius,
class: radius-rl,
values: (
null: $radius,
),
),
'radius-rls': (
property: border-top-left-radius border-bottom-right-radius,
class: radius-rl,
values: (
null: 0,
),
),
),
$utilities
);
@each $key, $utility in $utilities {
@if type-of($utility) == 'map' {
$values: map-get($utility, values);
@if type-of($values) == 'string' or type-of(nth($values, 1)) != 'list' {
$values: zip($values, $values);
}
@each $key, $value in $values {
$properties: map-get($utility, property);
@if type-of($properties) == 'string' {
$properties: append((), $properties);
}
$property-class: if(
map-has-key($utility, class),
map-get($utility, class),
nth($properties, 1)
);
$property-class: if($property-class == null, '', $property-class);
$property-class-modifier: if($key, if($property-class == '', '', '-') + $key, '');
.#{$property-class + $property-class-modifier} {
@each $property in $properties {
#{$property}: $value !important;
}
}
}
}
}

61
sheep/scss/_mixins.scss Normal file
View File

@@ -0,0 +1,61 @@
@mixin bg-square {
background: {
color: #fff;
image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%),
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%);
size: 40rpx 40rpx;
position: 0 0, 20rpx 20rpx;
}
}
@mixin flex($direction: row) {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: $direction;
}
@mixin flex-bar {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin arrow {
content: '';
height: 0;
width: 0;
position: absolute;
}
@mixin arrow-top {
@include arrow;
// border-color: transparent transparent $ui-BG;
border-style: none solid solid;
border-width: 0 20rpx 20rpx;
}
@mixin arrow-right {
@include arrow;
// border-color: transparent $ui-BG transparent;
border-style: solid solid solid none;
border-width: 20rpx 20rpx 20rpx 0;
}
@mixin position-center {
position: absolute !important;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
@mixin blur {
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
color: var(--ui-TC);
}

286
sheep/scss/_tools.scss Normal file
View File

@@ -0,0 +1,286 @@
/* ==================
常用工具
==================== */
.ss-bg-opactity-block {
background-color: rgba(#000, 0.2);
color: #fff;
}
/* ==================
flex布局
==================== */
.ss-flex {
display: flex;
flex-direction: row;
align-items: center;
}
.ss-flex-1 {
flex: 1;
}
.ss-flex-col {
display: flex;
flex-direction: column;
}
.ss-flex-wrap {
flex-wrap: wrap;
}
.ss-flex-nowrap {
flex-wrap: nowrap;
}
.ss-col-center {
align-items: center;
}
.ss-col-top {
align-items: flex-start;
}
.ss-col-bottom {
align-items: flex-end;
}
.ss-col-stretch {
align-items: stretch;
}
.ss-row-center {
justify-content: center;
}
.ss-row-left {
justify-content: flex-start;
}
.ss-row-right {
justify-content: flex-end;
}
.ss-row-between {
justify-content: space-between;
}
.ss-row-around {
justify-content: space-around;
}
.ss-self-start {
align-self: flex-start;
}
.ss-self-end {
align-self: flex-end;
}
.ss-self-center {
align-self: center;
}
.ss-h-100 {
height: 100%;
}
.ss-w-100 {
width: 100%;
}
/* ==================
margin padding: 内外边距
==================== */
@for $i from 0 through 100 {
// 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 {
// 得出u-margin-30或者u-m-30
.ss-margin-#{$i},
.ss-m-#{$i} {
margin: $i + rpx;
}
.ss-m-x-#{$i} {
margin-left: $i + rpx;
margin-right: $i + rpx;
}
.ss-m-y-#{$i} {
margin-top: $i + rpx;
margin-bottom: $i + rpx;
}
// 得出u-padding-30或者u-p-30
.ss-padding-#{$i},
.ss-p-#{$i} {
padding: $i + rpx;
}
.ss-p-x-#{$i} {
padding-left: $i + rpx;
padding-right: $i + rpx;
}
.ss-p-y-#{$i} {
padding-top: $i + rpx;
padding-bottom: $i + rpx;
}
@each $short, $long in l left, t top, r right, b bottom {
// 缩写版,结果如: u-m-l-30
// 定义外边距
.ss-m-#{$short}-#{$i} {
margin-#{$long}: $i + rpx;
}
// 定义内边距
.ss-p-#{$short}-#{$i} {
padding-#{$long}: $i + rpx;
}
// 完整版结果如u-margin-left-30
// 定义外边距
.ss-margin-#{$long}-#{$i} {
margin-#{$long}: $i + rpx;
}
// 定义内边距
.ss-padding-#{$long}-#{$i} {
padding-#{$long}: $i + rpx;
}
}
}
}
/* ==================
radius
==================== */
@for $i from 0 through 100 {
// 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 {
.ss-radius-#{$i},
.ss-r-#{$i} {
border-radius: $i + rpx;
}
.ss-r-t-#{$i} {
border-top-left-radius: $i + rpx;
border-top-right-radius: $i + rpx;
}
.ss-r-b-#{$i} {
border-bottom-left-radius: $i + rpx;
border-bottom-right-radius: $i + rpx;
}
@each $short, $long in tl 'top-left', tr 'top-right', bl 'bottom-right', br 'bottom-right' {
// 定义外边距
.ss-r-#{$short}-#{$i} {
border-#{$long}-radius: $i + rpx;
}
// 定义内边距
.ss-radius-#{$long}-#{$i} {
border-#{$long}-radius: $i + rpx;
}
}
}
}
/* ==================
溢出省略号
@param {Number} 行数
==================== */
@mixin ellipsis($rowCount: 1) {
// @if $rowCount <=1 {
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
// } @else {
// min-width: 0;
// overflow: hidden;
// text-overflow: ellipsis;
// display: -webkit-box;
// -webkit-line-clamp: $rowCount;
// -webkit-box-orient: vertical;
// }
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $rowCount;
-webkit-box-orient: vertical;
}
@for $i from 1 through 6 {
.ss-line-#{$i} {
@include ellipsis($i);
}
}
/* ==================
hover
==================== */
.ss-hover-class {
background-color: $gray-c;
opacity: 0.6;
}
.ss-hover-btn {
transform: translate(1px, 1px);
}
/* ==================
底部安全区域
==================== */
.ss-safe-bottom {
padding-bottom: 0;
padding-bottom: calc(constant(safe-area-inset-bottom) / 5 * 3);
padding-bottom: calc(env(safe-area-inset-bottom) / 5 * 3);
}
/* ==================
字体大小
==================== */
@for $i from 20 through 50 {
.ss-font-#{$i} {
font-size: $i + rpx;
}
}
/* ==================
按钮
==================== */
.ss-reset-button {
padding: 0;
margin: 0;
font-size: inherit;
background-color: transparent;
color: inherit;
position: relative;
border: 0rpx;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
box-sizing: border-box;
text-align: center;
text-decoration: none;
white-space: nowrap;
vertical-align: baseline;
transform: translate(0, 0);
}
.ss-reset-button.button-hover {
transform: translate(1px, 1px);
background: none;
}
.ss-reset-button::after {
border: none;
}

165
sheep/scss/_var.scss Normal file
View File

@@ -0,0 +1,165 @@
@import './mixins';
//颜色 渐变背景60%
$red: #d10019; // 中国红
$orange: #f37b1d; // 桔橙
$gold: #fbbd08; // 明黄
$green: #8dc63f; // 橄榄绿
$cyan: #1cbbb4; // 天青
$blue: #0081ff; // 海蓝
$purple: #6739b6; // 姹紫
$brightRed: #e54d42; // 嫣红
$forestGreen: #39b54a; // 森绿
$mauve: #9c26b0; // 木槿
$pink: #e03997; // 桃粉
$brown: #a5673f; // 棕褐
$grey: #8799a3; // 玄灰
$gray: #aaaaaa; // 草灰
$black: #333333; // 墨黑
$primary: #0055A2; // 主题蓝
$colors: ();
$colors: map-merge(
(
'primary':$primary,
'red':$red,
'orange':$orange,
'gold':$gold,
'green':$green,
'cyan':$cyan,
'blue':$blue,
'purple':$purple,
'brightRed':$brightRed,
'forestGreen':$forestGreen,
'mauve':$mauve,
'pink':$pink,
'brown':$brown,
'grey':$grey,
'gray':$gray,
'black':$black,
),
$colors
);
//灰度
$bg-page: #f6f6f6;
$white: #ffffff;
$gray-f: #f8f9fa;
$gray-e: #eeeeee;
$gray-d: #dddddd;
$gray-c: #cccccc;
$gray-b: #bbbbbb;
$gray-a: #aaaaaa;
$dark-9: #999999;
$dark-8: #888888;
$dark-7: #777777;
$dark-6: #666666;
$dark-5: #555555;
$dark-4: #484848; //ss-黑
$dark-3: #333333;
$dark-2: #222222;
$dark-1: #111111;
$black: #000000;
$grays: ();
$grays: map-merge(
(
'white': $white,
'gray-f': $gray-f,
'gray-e': $gray-e,
'gray-d': $gray-d,
'gray-c': $gray-c,
'gray-b': $gray-b,
'gray-a': $gray-a,
'gray': $gray-a,
),
$grays
);
$darks: ();
$darks: map-merge(
(
'dark-9': $dark-9,
'dark-8': $dark-8,
'dark-7': $dark-7,
'dark-6': $dark-6,
'dark-5': $dark-5,
'dark-4': $dark-4,
'dark-3': $dark-3,
'dark-2': $dark-2,
'dark-1': $dark-1,
'black': $black,
),
$darks
);
// 边框
$border-width: 1rpx !default; // 边框大小
$border-color: $gray-d !default; // 边框颜色
// 圆角
$radius: 10rpx !default; // 默认圆角大小
$radius-lg: 40rpx !default; // 大圆角
$radius-sm: 6rpx !default; // 小圆角
$round-pill: 1000rpx !default; // 半圆
// 动画过渡
$transition-base: all 0.2s ease-in-out !default; // 默认过渡
$transition-base-out: all 0.04s ease-in-out !default; // 进场过渡
$transition-fade: opacity 0.15s linear !default; // 透明过渡
$transition-collapse: height 0.35s ease !default; // 收缩过渡
// 间距
$spacer: 20rpx !default;
$spacers: () !default;
$spacers: map-merge(
(
0: 0,
1: $spacer * 0.25,
2: $spacer * 0.5,
3: $spacer,
4: $spacer * 1.5,
5: $spacer * 3,
6: $spacer * 5,
),
$spacers
);
// 字形
$font-weight-lighter: lighter !default;
$font-weight-light: 300 !default;
$font-weight-normal: 400 !default;
$font-weight-bold: 700 !default;
$font-weight-bolder: 900 !default;
$fontsize: () !default;
$fontsize: map-merge(
(
xs: 20,
sm: 24,
df: 28,
lg: 32,
xl: 36,
xxl: 44,
sl: 80,
xsl: 120,
),
$fontsize
);
// 段落
$line-height-base: 1.5 !default;
$line-height-lg: 2 !default;
$line-height-sm: 1.25 !default;
// 图标
$iconsize: () !default;
$iconsize: map-merge(
(
xs: 0.5,
sm: 0.75,
df: 1,
lg: 1.25,
xl: 1.5,
xxl: 2,
sl: 6,
xsl: 10,
),
$iconsize
);

Binary file not shown.

File diff suppressed because one or more lines are too long

185
sheep/scss/icon/_icon.scss Normal file
View File

@@ -0,0 +1,185 @@
@font-face {
font-family: 'colorui'; /* Project id 2620914 */
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAygAAoAAAAAIkgAAAxSAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAhGoKsVymRQtiAAE2AiQDgTgEIAWEeQeDYBuZGyOSbVae7K8OeCJiD68QRhv2RMBKosMmNK8qxK/ut4Gha0N1U0/9oEQyafFEOARh6+uIEw6hYYQksz+Pc/Z+k1pKqGBdOtYQzNsiXtw60YJvjh3MKKcp7BTzMBFnZz4zLzz/rf3eHVmbfTv/Y5K0SuhUkieoIjZ1lxCJjUPIdKIR5Lb7jdAYHdiYwVLMjWG1W23JaBYEN+UEpRxpEewlsA3FpM60rpuceqKHYaFtKRhC7DQpsRe5ZWk1PII5j7gFNnx4Hq55EfHf1aJBBkSlNsAJVf39mmn3B45QlWALjoQ8V3by7e9us/t/NrS56eWAMeFNCkAWSJikeESqZWFboYmkrK+TFVbyIp4VBmZg+uY+xLNyZlJHJfbOvWcBOPaQwYPwzOkfAfh21QTdE00UIRXgMdgky8K3h5yyxBI4BHDbmE0bmYKdFxMA5E1pTMb2dKyaCqiEMk5xKOWVhizQAx8aEBCDwjQYBdOkUkQtLINc5+Dk4mYweflYFjNzCyuDteqYSVY4x0/d3/ybcVsAAJEoBBlP6Z/oX19uO2gimvKfpbAsy7RUKufMf7xzjztuj/8efzPuzYyMKQkTfDxcBnJ5kcmDVK5yIcaTs1yPnY9tj70U3tYnBcimgEw4xmJCGs8fINHooop2mYeY/COxulAXukS7zIgVGpxY8djxVswyxYvjOIK3O7GgTHYEwrYyy/pB2k3StXYhDOqBwB2IQ0pV7XddeXAMS8ljuzIuLTm8ciwPAh27ekO93RRSJIK7t3YplxaFJUYOx8ZGWekuOlqNqPE8G/xGLleUaklSq2JF643wgMegYQx5GFwfxWPY031aOxc5BTxqJP1OIikJ5YT4TgaNmChTHeQ+lq97jScSQ8W1p6oqKaoifWPwRQR8al5ykmBlpZxlIncn+o47rBmcJbGnC6rdArFlA1Yha4JaisVMtPrCDb6aqs2thW06dt3mRpVaNPdQ2MB6IDya+jcGd6LNyYZ/S66nX8yvB6063mJrTkhv0pypWztxoskyFYsQuUn+5LCTwheR3rwk5yEKzh4jkUuyhR3ESSYRVPuVYlaLPaLNpEwBEMNNAbYgjB+8lXKV3FEpG/gcQa2YlbyGxUGbgqRhClEeUnR5VJBGTsJMG5nxTPQQiThlsOwO8/yBly3d3+YkxSbufAH7EH1BCDFBbLv+zlxoYVYMdCcjVo7ztywchZS9EFZhxkSOs8hiOdfTBUVHzhJixbESV4pCFovPIRpzfBMVyZqASsvhSZjs8mvouSkIqoRzJJSEn+LoVCOCEs1kBA2CFF3Auj0LKKkqmvSD5IgMiKJpeE5rd9nM3t8rV6F6+x50IioIznenwdn4uu5XU1LvxbqREJjvagZsasFZCvPWoqD6ZLpizjtbfNXC0m68y72UC3wGrgJW7Q35nUw6kGb9LmlPF6zJdm38fqVVPTz1Yvc5ztymg3m9zr0egHRJzinJy6xbYIfqXWFRY5JKXLo18XJ8ii7ijdJ5GACach62+uDPfcHpePCiOe5phs1PVethqFMgVUIy/Y6hsflh6BMamVBmSpDOH0f4Bhpu7VHudb9NuofAp4NOKnZdKHSFb0Aa/Qk2gFpzja5gq1MmCTQ2drzkEwYuKIN8ENVcYkUD7yzoi4JiH78dj/DKOOJla8U/5SRz+b3KobZdoqm03HHKMVVGdVKHKkhPZI3BBiWvJq1uwLWE+BrPKerKyEqFe74EimsyXSiIzrCyeiFfDYYN88CM9tqAw69WAZnWwOgm8o4aCJC8UQecoCaI36IPMoiAE7lHCqehkZjRZAkijuccLoCGP//YeETAUOT+BAKqMlKwDfZ9Nj1txYqVK9LSJk0CWxcwrUQaa3/a95CL294tXrokjtsKwgXr0as6RDITDDm7NFgikyS8Fo4/EpHZEjZ7VlhLVwcAK+nRmVBt6IE/TOXT+PjlgwdCg3Botq+gALyOb6DUet1fA/qD2tNLmajePRQFVMiE9BRsEiRiT1hqzVFRXdRJ/b3wnK2jJkRjCgvhjxsLZnx4HHSZzBd38twQynBM6AbPixe9KT2rUX/Z7vPYFkKQzOkREyZ+yLSSl+magkHCqiyl+infSAbOcM6B1xbGJWrhRZKRZy+o5wWMel5SU3t6db17dMfdEyBhofFrbf3HmXX5e8sWWGX/zz/tc2EjQdCVVSEhslK6VAYpBtrggoUHn6ViIUsxVJdIKCgLVU6qLgGl2CWwTREReTEI0Fj0XXlkSXh4SWT50w4AVtiHfjo9YEJ/9qN3/EH7HKZWWwySL8XlWuXGaWDpMlPMEFtWeeceq5ITYIn5VkGOiYhgKmIqIsSIfein0/DCVn3FRF2hRlOom3i4A4AV9qEPT2cwV8Sq2Dv3yirZIYYyL20CTYSvcaDcgnWJgcb00RRGpbKqF5t2s2Yz20XNVvNX2p3tO4pzdbjJ/FV3SVnNbCNS3MF+xZaFBDNpvlXMLZpfEl7oahnVoLU0uzRorZhQMH0vMS0NaRy63LbMZCfd46gm0EzYGlkxQStoqXbCYe0E2Ic+PN1ZvqF7HOB/reVEknDe0JCe6l1DxbocSj/kSPTHHssa9UC25vJAgD3Ey0o5HF1rZhxPjk5HimH6JA3BFvSpHc6Pmr91dkxOViypOOSaBi+Jb2XtNOFkfbkA7HqpsOG1F7kHpj/yZax3Z30Ql1BVr+n01nxqrvMpVhY3aLVNf44etpY3HfK/fc02Hcsaf7k8YAKs5VOTC8i7xBvq1GEO66L/dg9T120Wl5K+aMw0mvzg3xAReLZWU5VcVR/cmpCqFBP0kctDl39TXZpWea/PP7JWMdFhl/1uc2jCSrkCOox+7RsN6uF9WtgKLQoxPUNQui19qtP+dOHky+51xNOt2P3XceYZAgMNALBiza2tFrRH91meZGKWxjPx6qUYfmriTU8nXG93J8kCAj9V6SLnN/eENWMspqWbV73me08aK6WzntNX6edZ+nN4aJwVugkag0EzQXe4A4AV9mErHJlwxjExobY2oVmCJM1TmuhgqvCvgWA04wBn+YBR6T3AKkw9dRkfIG/EBvl0zTdNt+fGtDtEcA4j7fNFXiudsm1Nbys1DY3K17y4yXHM7VOxJuBbpe/SEQcuwqFVEzjme1dGXaqdcESbYIv2SMerpiNZGKR8bW6iR7ybW7xHIueRAKvGuye6cwnunLQmcDu4dHJL8NjLq6/fJ7qCDtMrsQQsVGc+3nyZ3WmSeWsEsquNshs2MIE8PJef36Yme9xZWGgIq337jbxM/s3ezYBWezurmt06cnNnAyinu1/5PtrqKLS3d4D+VHMok9FmouH4YRkAcPkdcdxqqHgOZWh0mVBzcwAgO84gE2mXUSan/aEzE3QMyuDiXH9eD8cNnoQ4YgwbI5reqXT8vHK6NPJfFHC94vyLi93n+8ssrJPZegAQ4TFsfwc4G/Aq8EBctsRLuw9+1gCDM29iUZFj98y6BLTHgkkfjUxoBC1Gh/RxGehd3eQoVvUMzZksE11i0WfIjAqZIQVt5wJaZFuciWUBMIkAZ1UiBAtgAQUKuKgiO/7OImJ2LI/QOBYj5opwvR/LJxHCCojFsQRvSG8sYQN1MBubxHleYFZZRAHbWB5rv7IYBRxDuP4Gy2cPr1gBBSJZgiHU15FwQ7XEl2MxSIFBFErIG7Aq/MOBYWy4CF9uKAj0C7C7fF21eXVduaampuxTCvi72Rgq3PEvVoJsWC5zw9BKRCDEq1LKqGZcvQQNRRrWr1RsnrnKcmoQuDRbmqvcpTyyo1wtgowe/DxoyAA8vVrlrdEQCH4QQIKSyVwy90lqWq1OOQ2SWqPMUaaKS3MWOSmROaVmj3JomO8uzbxIxViOH1QwQOEIB/82VBlpjh8UC7HzbOIIG0CkALkKAMKLlxZDOOIjARIiERIjAkmQFSKRNZIiGXIU2GCLHfY4oGQMFCrG4sg41NA4weCMC6644Y4HnnjhjQ+++DnwSBYCToSQXVFfqZvK6pblO4+PQ7B9IgbOiK+sSSvMrp2Pw3oS/pJtruAAWQSXUlE+5FWDGUtK+NIpgkNkVOLAc1OTd6BTSQDB1aBjVRZCmrL4DBci4MMBL8HTibgjBsaAa3CTSAIrYc5EIJCgfI6lYmNJCW+z5Aq4UQCAykKJHMGzbRRAl0YBOEcEsCFkafkER+6NVxcb5wpr51YmhgYlnnVJicEBJBAAAA==')
format('woff2');
/* #ifdef MP-ALIPAY */
src: url('//at.alicdn.com/t/font_2620914_57y9q5zpbel.woff?t=1624238023908') format('woff'),
url('//at.alicdn.com/t/font_2620914_57y9q5zpbel.ttf?t=1624238023908') format('truetype');
/* #endif */
}
[class*='_icon-'] {
font-family: 'colorui' !important;
display: inline-block;
}
@font-face {
font-family: 'ui-num';
src: url('data:application/x-font-ttf;base64,AAEAAAAKAIAAAwAgT1MvMla+dCkAAACsAAAAYGNtYXAQUxhKAAABDAAAAVJnbHlmS86JUQAAAmAAAAUUaGVhZA7I1xIAAAd0AAAANmhoZWEFqgF3AAAHrAAAACRobXR4BycBzgAAB9AAAAAibG9jYQZmB5wAAAf0AAAAHG1heHAAEQBDAAAIEAAAACBuYW1lGVKlzAAACDAAAAGtcG9zdADDAJYAAAngAAAAPAAEAewBkAAFAAACmQLMAAAAjwKZAswAAAHrADMBCQAAAgAGAwAAAAAAAAAAAAEQAAAAAAAAAAAAAABQZkVkAMAALAA5Ayz/LABcAywA1AAAAAEAAAAAAxgAAAAAACAAAQAAAAMAAAADAAAAHAABAAAAAABMAAMAAQAAABwABAAwAAAACAAIAAIAAAAsAC4AOf//AAAALAAuADD////V/9T/0wABAAAAAAAAAAAAAAEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAgADBAUGBwgJCgsMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAiAAABMgKqAAMABwAANxEhESczESMiARDuzMwAAqr9ViICZgAAAAEAUP9hAMcAdwADAAAXNSMRx3c9tP7qAAEAUAAAAM0AfQADAAA3NSMVzX0AfX0AAAIAPv/6AeMC3wASACQAACUDJicmJwYHBgcRFhcWFzY3NjcHFAcGByYnJjURNDc2NxYXFhUB7wwCPDxZWTs7AwM7O1lZPDwOdB0bMzIbHBwbMjMbHdABPmM3NgEBNjdj/r1jNzYBATY3aAI2ICABASAgNgE9Nx8gAQEgHzcAAAAAAQB1AAABbALZAAYAACURIwcVNxEBbGmOjgAC2Xt0ff2ZAAAAAQBBAAAB6ALfAB4AACU1IRM2NzY1JicmJwYHBgczNjc2FxYXFhUUBwYHARUB6P7X5SIREQE5OV9fOjkCaAIfHywzGxwJCRX+6ABdARgoJCIvYDY2AQE3N189GhsBAR4dMxoYFhn+q10AAAAAAQAr//gB6QLgADUAACUmJyYnNjc2NSYnJicGBwYHMzY3NjMyFxYXFAcGByMVMxYXFhUGBwYjIicmJyMWFxY3Mjc2NwH1DRocLysYGAI5O15ZOzwGaQQcHTAuHh8BGxw4ERE+Hh4BISE0LyIhBWgGQD9aXkA/DtI+KioVFCcmOl03NwEBNDNeMRscHRw4Mh0eAVsBHyA4Oh8gGxk7azEyATU1bwABACQAAAH+AtkADgAAJTUjNSMVIwEjARUhFTM1Af5OZbUBAHH+/wEnZW5hqqoCCv32YW5uAAAAAAEAQf/5AewC2QA3AAAlJicmJyYnJiMiBwYHNSE1IREzNjc2NxYXFgcWBwYHBgcGIyInJicjFhcWFxYXFhc2NzY3Njc2NwH2Cg0MKBcgISsoHx8TASv+d18IGhosPRgWAQEHBhcOExMYMRkaBmgCDAwdFygoNDYmJRknDAwK+i4yMioXDAwLCxTBXf5yGxMSAQErKkIlIiIXDwcHGxkxJiQjHhgQDwEBDxEYKDAvQQAAAgA5//oB6ALZABcAKAAAJSYnJiciBwYHEyMDBgcGFRYXFhc2NzY3BwYHBgcmJyYnNjc2MxYXFhcB9A42NlERERAPnW+mGQ4QAjs7YGE6Og5rCh4eMzIdHgEBHh0yNR0eCd1cOTgBAgMGATn+ri8sLCxmOjkBATs8awJAISIBASIhOzshIgEjIzIAAAABAEEAAAHzAtkACAAAATUhFTM1MwMzAfP+TmTe9XECfF3Qc/2EAAAAAwAw//oB8gLfACAAMQBCAAAlJicmJzY3NjcmJyYnBgcGBxYXFhcGBwYHFhcWFzY3NjcnBgcGByYnJic2NzY3FhcWFwMGBwYHJicmJzY3NjcWFxYXAf4NHh4oJRkZAQI7PFxbOzwCARoZJCceHgECQD5gYT9ADmwLIiA1NCEhAQEhITQ1ICILDAoeHTEwHR0BAR0dMDEdHgrTOyoqFxUnJzpcNjYBATY2XDonJxUXKipAZTc3AQE3N2oCOSIiAQEiIjQ0IiMBASMiLwFKPh4eAQEeHjEyHh8BAR8eJQAAAAACADkAAAHoAt8AFwAoAAABJicmJwYHBgcWFxYXMjc2NwMzEzY3NjcHBgcGIyYnJjU2NzY3FhcWFwH0Djo7YWA6OwICNjZRERERDpxvphkODwxrCh4eMzQdHQEeHTIzHh4KAhJaOTkBATs8ZmE5OAEDAgb+xwFSLywsOQNHISIBIyM3OyIhAQEhIi8AAAEAAAABAADHiynwXw889QALBAAAAAAA1sTJ5wAAAADWxMntACL/YQH+AuAAAAAIAAIAAAAAAAAAAQAAAyz/LABcAiIAIgAkAf4AAQAAAAAAAAAAAAAAAAAAAAQBdgAiARcAUAEdAFACIgA+AHUAQQArACQAQQA5AEEAMAA5AAAAAAAUACAALABsAH4AtAEGASIBegHAAdQCRAKKAAEAAAANAEMAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAJYAAQAAAAAAAQAKAAAAAQAAAAAAAgAGAAoAAQAAAAAAAwAbABAAAQAAAAAABAAKACsAAQAAAAAABQAeADUAAQAAAAAABgAKAFMAAwABBAkAAQAUAF0AAwABBAkAAgAMAHEAAwABBAkAAwA2AH0AAwABBAkABAAUALMAAwABBAkABQA8AMcAAwABBAkABgAUAQNmb250ZWRpdG9yTWVkaXVtRm9udEVkaXRvciAxLjAgOiBmb250ZWRpdG9yZm9udGVkaXRvclZlcnNpb24gMS4wOyBGb250RWRpdG9yICh2MS4wKWZvbnRlZGl0b3IAZgBvAG4AdABlAGQAaQB0AG8AcgBNAGUAZABpAHUAbQBGAG8AbgB0AEUAZABpAHQAbwByACAAMQAuADAAIAA6ACAAZgBvAG4AdABlAGQAaQB0AG8AcgBmAG8AbgB0AGUAZABpAHQAbwByAFYAZQByAHMAaQBvAG4AIAAxAC4AMAA7ACAARgBvAG4AdABFAGQAaQB0AG8AcgAgACgAdgAxAC4AMAApAGYAbwBuAHQAZQBkAGkAdABvAHIAAAAAAgAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAADQANAAAADwARABMAFAAVABYAFwAYABkAGgAbABw=')
format('woff2');
font-weight: normal;
font-style: normal;
}
._icon-checkbox:before {
content: '\e713';
}
._icon-box:before {
content: '\e714';
}
._icon-checkbox-o:before {
content: '\e715';
}
._icon-round:before {
content: '\e716';
}
._icon-home-o:before {
content: '\e70a';
}
._icon-home:before {
content: '\e70d';
}
._icon-edit:before {
content: '\e649';
}
._icon-close:before {
content: '\e6ed';
}
._icon-check-round:before {
content: '\e6f1';
}
._icon-check-round-o:before {
content: '\e6f2';
}
._icon-close-round:before {
content: '\e6f3';
}
._icon-close-round-o:before {
content: '\e6f4';
}
._icon-waiting:before {
content: '\e6f8';
}
._icon-waiting-o:before {
content: '\e6f9';
}
._icon-warn:before {
content: '\e662';
}
._icon-warn-o:before {
content: '\e675';
}
._icon-more:before {
content: '\e688';
}
._icon-delete:before {
content: '\e707';
}
._icon-delete-o:before {
content: '\e709';
}
._icon-add-round:before {
content: '\e717';
}
._icon-add-round-o:before {
content: '\e718';
}
._icon-add:before {
content: '\e6e4';
}
._icon-info:before {
content: '\e6ef';
}
._icon-info-o:before {
content: '\e705';
}
._icon-move:before {
content: '\e768';
}
._icon-title:before {
content: '\e82f';
}
._icon-titles:before {
content: '\e745';
}
._icon-loading:before {
content: '\e746';
}
._icon-copy-o:before {
content: '\e7bc';
}
._icon-copy:before {
content: '\e85c';
}
._icon-loader:before {
content: '\e76d';
}
._icon-search:before {
content: '\e782';
}
._icon-back:before {
content: '\e600';
}
._icon-forward:before {
content: '\e601';
}
._icon-arrow:before {
content: '\e608';
}
._icon-drop-down:before {
content: '\e61c';
}
._icon-drop-up:before {
content: '\e61d';
}
._icon-check:before {
content: '\e69f';
}
._icon-move-round:before {
content: '\e602';
}
._icon-move-round-o:before {
content: '\e603';
}
._icon-scan:before {
content: '\e85d';
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
@import './icon'; //核心图标库
@import './coloricon'; //扩展图标库
@import './sheepicon';
.icon-spin {
animation: icon-spin 2s infinite linear;
}
.icon-pulse {
animation: icon-spin 1s infinite steps(8);
}
@keyframes icon-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.icon-90 {
transform: rotate(90deg);
}
.icon-180 {
transform: rotate(180deg);
}
.icon-270 {
transform: rotate(270deg);
}
.icon-x {
transform: scale(-1, 1);
}
.icon-y {
transform: scale(1, -1);
}
.icon-fw {
width: calc(18em / 14);
text-align: center;
}
@each $class, $value in $iconsize {
.icon-#{$class} {
transform: scale(#{$value});
}
}

28
sheep/scss/index.scss Normal file
View File

@@ -0,0 +1,28 @@
@import './tools';
@import './ui';
@import './tabbar-fix';
/* 字体文件 */
@font-face {
font-family: OPPOSANS;
src: url('~@/sheep/scss/font/OPPOSANS-M-subfont.ttf');
}
.font-OPPOSANS {
font-family: OPPOSANS;
}
page {
-webkit-overflow-scrolling: touch; // 解决ios滑动不流畅
height: 100%;
width: 100%;
// font-family: OPPOSANS;
word-break: break-all; //英文文本不换行
white-space: normal;
background-color: $bg-page;
color: $dark-3;
}
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
display: none;
}

View File

@@ -0,0 +1,48 @@
/**
* 主题相关的SCSS混合器
* 统一管理所有与主题色 #0055A2 相关的样式
*/
// 主题色变量
$theme-primary: #0055A2 !default;
$theme-primary-light: #337AB7 !default;
$theme-primary-dark: #003F73 !default;
$theme-primary-gradient: rgba(0, 85, 162, 0.6) !default;
// 渐变混合器 - 水平渐变
@mixin gradient-horizontal($start: $theme-primary, $end: $theme-primary-gradient) {
background: linear-gradient(90deg, $start, $end);
}
// 渐变混合器 - 垂直渐变
@mixin gradient-vertical($start: $theme-primary, $end: $theme-primary-gradient) {
background: linear-gradient(180deg, $start 0%, $end 100%);
}
// 渐变混合器 - 对角渐变
@mixin gradient-diagonal($start: $theme-primary, $end: $theme-primary-gradient) {
background: linear-gradient(135deg, $start, $end);
}
// 主题按钮样式
@mixin theme-button() {
@include gradient-horizontal();
color: #ffffff;
border: none;
border-radius: 40rpx;
font-weight: 500;
transition: all 0.3s ease;
&:active {
@include gradient-horizontal($theme-primary-dark, rgba(0, 63, 115, 0.6));
transform: scale(0.98);
}
}
// 主题头部渐变
@mixin theme-header() {
@include gradient-vertical();
color: #ffffff;
padding: 0 0 120rpx 0;
box-sizing: border-box;
}

View File

View File

@@ -0,0 +1,204 @@
/* ==================
背景
==================== */
/* -- 基础色 -- */
@each $color, $value in map-merge($colors, $darks) {
.bg-#{$color} {
background-color: $value !important;
@if $color == 'yellow' {
color: #333333 !important;
} @else {
color: #ffffff !important;
}
}
}
/* -- 浅色 -- */
@each $color, $value in $colors {
.bg-#{$color}-light {
background-image: linear-gradient(45deg, white, mix(white, $value, 85%)) !important;
color: $value !important;
}
.bg-#{$color}-thin {
background-color: rgba($value, var(--ui-BG-opacity)) !important;
color: $value !important;
}
}
/* -- 渐变色 -- */
@each $color, $value in $colors {
@each $colorsub, $valuesub in $colors {
@if $color != $colorsub {
.bg-#{$color}-#{$colorsub} {
// background-color: $value !important;
background-image: linear-gradient(130deg, $value, $valuesub) !important;
color: #ffffff !important;
}
}
}
}
.bg-yellow-gradient {
background-image: linear-gradient(45deg, #f5fe00, #ff6600) !important;
color: $dark-3 !important;
}
.bg-orange-gradient {
background-image: linear-gradient(90deg, #ff6000, #fe832a) !important;
color: $white !important;
}
.bg-red-gradient {
background-image: linear-gradient(45deg, #f33a41, #ed0586) !important;
color: $white !important;
}
.bg-pink-gradient {
background-image: linear-gradient(45deg, #fea894, #ff1047) !important;
color: $white !important;
}
.bg-mauve-gradient {
background-image: linear-gradient(45deg, #c01f95, #7115cc) !important;
color: $white !important;
}
.bg-purple-gradient {
background-image: linear-gradient(45deg, #9829ea, #5908fb) !important;
color: $white !important;
}
.bg-blue-gradient {
background-image: linear-gradient(45deg, #00b8f9, #0166eb) !important;
color: $white !important;
}
.bg-cyan-gradient {
background-image: linear-gradient(45deg, #06edfe, #48b2fe) !important;
color: $white !important;
}
.bg-green-gradient {
background-image: linear-gradient(45deg, #3ab54a, #8cc63f) !important;
color: $white !important;
}
.bg-olive-gradient {
background-image: linear-gradient(45deg, #90e630, #39d266) !important;
color: $white !important;
}
.bg-grey-gradient {
background-image: linear-gradient(45deg, #9aadb9, #354855) !important;
color: $white !important;
}
.bg-brown-gradient {
background-image: linear-gradient(45deg, #ca6f2e, #cb1413) !important;
color: $white !important;
}
@each $color, $value in $grays {
.bg-#{$color} {
background-color: $value !important;
color: #333333 !important;
}
}
.bg-square {
@include bg-square;
}
.bg-none {
background: transparent !important;
color: inherit !important;
}
[class*='bg-mask'] {
position: relative;
//background: transparent !important;
color: #ffffff !important;
> view,
> text {
position: relative;
z-index: 1;
color: #ffffff;
}
&::before {
content: '';
border-radius: inherit;
width: 100%;
height: 100%;
@include position-center;
background-color: rgba(0, 0, 0, 0.4);
z-index: 0;
}
@at-root .bg-mask-80::before {
background: rgba(0, 0, 0, 0.8) !important;
}
@at-root .bg-mask-50::before {
background: rgba(0, 0, 0, 0.5) !important;
}
@at-root .bg-mask-20::before {
background: rgba(0, 0, 0, 0.2) !important;
}
@at-root .bg-mask-top::before {
background-color: rgba(0, 0, 0, 0);
background-image: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.618), rgba(0, 0, 0, 0.01));
}
@at-root .bg-white-top {
background-color: rgba(0, 0, 0, 0);
background-image: linear-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.3));
}
@at-root .bg-mask-bottom::before {
background-color: rgba(0, 0, 0, 0);
background-image: linear-gradient(rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 0.618), rgba(0, 0, 0, 1));
}
}
.bg-img {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
[class*='bg-blur'] {
position: relative;
> view,
> text {
position: relative;
z-index: 1;
}
&::before {
content: '';
width: 100%;
height: 100%;
@include position-center;
border-radius: inherit;
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
}
}
@supports (-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px)) {
.bg-blur::before {
@include blur;
background-color: var(--ui-Blur-1);
}
.bg-blur-1::before {
@include blur;
background-color: var(--ui-Blur-2);
}
.bg-blur-2::before {
@include blur;
background-color: var(--ui-Blur-3);
}
}
@supports not (backdrop-filter: blur(5px)) {
.bg-blur {
color: var(--ui-TC);
&::before {
background-color: var(--ui-BG);
}
}
.bg-blur-1 {
color: var(--ui-TC);
&::before {
background-color: var(--ui-BG-1);
}
}
.bg-blur-2 {
color: var(--ui-TC);
&::before {
background-color: var(--ui-BG-2);
}
}
}

View File

@@ -0,0 +1,140 @@
/* ==================
边框
==================== */
/* -- 实线 -- */
.border {
overflow: initial !important;
@at-root [class*='border'],
[class*='dashed'] {
position: relative;
&.dline {
--ui-Border: var(--ui-BG-3);
}
&::after {
content: ' ';
width: 200%;
height: 200%;
position: absolute;
z-index: 0;
top: 0;
left: 0;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
border-radius: inherit;
}
&.radius::after {
border-radius: calc(#{$radius} * 2);
}
&.round::after {
border-radius: #{$round-pill};
}
}
&::after {
border: 1px solid var(--ui-Border);
}
&s::after {
border: 4rpx solid var(--ui-Border);
}
&ss::after {
border: 8rpx solid var(--ui-Border);
}
@each $value in (top, right, bottom, left) {
&-#{$value}::after {
border-#{$value}: 1px solid var(--ui-Border);
}
&s-#{$value}::after {
border-#{$value}: 4rpx solid var(--ui-Border);
}
&ss-#{$value}::after {
border-#{$value}: 8rpx solid var(--ui-Border);
}
}
}
/* -- 虚线 -- */
.dashed {
&::after {
border: 4rpx dashed var(--ui-Border);
}
&s::after {
border: 6rpx dashed var(--ui-Border);
}
@each $value in (top, right, bottom, left) {
&-#{$value}::after {
border-#{$value}: 4rpx dashed var(--ui-Border);
}
&s-#{$value}::after {
border-#{$value}: 6rpx dashed var(--ui-Border);
}
}
}
@each $color, $value in map-merge($colors, map-merge($darks, $grays)) {
.border-#{$color}::after,
.border-#{$color}[class*='-shine']::before {
border-color: $value !important;
}
}
@each $value in (a, b, c, d, e) {
.main-#{$value}-border::after,
.main-#{$value}-border[class*='-shine']::before {
border-color: var(--main-#{$value}) !important;
}
}
.dashed-shine,
.dasheds-shine {
position: relative;
overflow: hidden;
&::after,
&::before {
border-style: dashed;
border-color: var(--ui-Border);
animation: shineafter 1s infinite linear;
width: calc(200% + 40px);
height: 200%;
border-width: 2px 0;
}
&::before {
content: ' ';
position: absolute;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
animation: shinebefore 1s infinite linear;
width: 200%;
height: calc(200% + 40px);
border-width: 0 2px;
}
}
.dasheds-shine {
&::after,
&::before {
border-width: 4px 0;
}
&::before {
border-width: 0 4px;
}
}
@keyframes shineafter {
0% {
top: 0;
left: -22px;
}
100% {
top: 0px;
left: 0px;
}
}
@keyframes shinebefore {
0% {
top: -22px;
left: 0;
}
100% {
top: 0px;
left: 0px;
}
}

View File

@@ -0,0 +1,87 @@
.ui-btn-box {
display: inline-block;
}
.ui-btn {
position: relative;
border: 0rpx;
display: inline-block;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 0.7857em 1.5em 0.7857em;
font-size: 28rpx;
line-height: 1em;
text-align: center;
text-decoration: none;
overflow: visible;
margin: 0 0.25em 0 0;
transform: translate(0rpx, 0rpx);
border-radius: $radius;
white-space: nowrap;
color: var(--text-a);
background-color: var(--ui-BG);
vertical-align: baseline;
&:first-child:last-child {
margin: 0;
}
&:not([class*='round'])::after {
border-radius: calc(#{$radius} * 2);
}
&:not([class*='border'])::after {
// content: ' ';
// width: 200%;
// height: 200%;
// display: block;
// position: absolute;
// z-index: 0;
// top: 0;
// left: 0;
// transform: scale(0.5);
// transform-origin: 0 0;
// pointer-events: none;
// box-sizing: border-box;
display: none;
}
&.round::after {
border-radius: #{$round-pill};
}
&.icon {
padding: 0.8em 0.8em;
}
&.sm {
font-size: 24rpx;
}
&.lg {
font-size: 32rpx;
}
&.xl {
font-size: 36rpx;
}
&.block {
width: 100%;
display: block;
font-size: 32rpx;
}
&[disabled] {
opacity: 0.6;
}
&.none-style {
background-color: transparent !important;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
}
}
.ui-btn:not(.icon) [class*='icon-'] {
margin: 0 0.25em;
}

353
sheep/scss/style/_card.scss Normal file
View File

@@ -0,0 +1,353 @@
/* ==================
卡片
==================== */
.ui-cards {
display: block;
overflow: hidden;
& .ui-btn.badge {
top: 0;
right: 0;
font-size: 24rpx;
padding: 0rpx 15rpx;
height: 40rpx;
}
&.no-card > .ui-item {
margin: 0rpx;
border-radius: 0rpx;
}
& > .ui-item {
display: block;
overflow: hidden;
border-radius: 10rpx;
margin: 30rpx;
}
& > .ui-item.shadow-blur {
overflow: initial;
}
.grid.grid-square {
margin-bottom: -20rpx;
}
&.article {
display: block;
& > .ui-item {
padding: 30rpx;
background-color: var(--box-bg);
display: flex;
align-items: flex-start;
}
& > .time {
padding: 30rpx 0 0 30rpx;
}
& > .ui-item .title {
font-size: 30rpx;
font-weight: 900;
color: #333333;
}
& > .ui-item .content {
flex: 1;
}
& > .ui-item > image {
width: 240rpx;
height: 6.4em;
margin-left: 20rpx;
border-radius: 6rpx;
}
& > .ui-item .content .desc {
font-size: 12px;
color: var(--text-c);
}
& > .ui-item .content .text-content {
font-size: 28rpx;
color: #888;
}
}
&.case {
.image {
position: relative;
image {
width: 100%;
display: block;
}
.ui-tag {
position: absolute;
right: 0;
top: 0;
}
.ui-bar {
position: absolute;
bottom: 0;
width: 100%;
background-color: transparent;
padding: 0rpx 30rpx;
}
.bg-black {
position: absolute;
bottom: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.6);
}
}
&.no-card .image {
margin: 30rpx 30rpx 0;
overflow: hidden;
border-radius: 10rpx;
}
}
&.dynamic {
display: block;
& > .ui-item {
display: block;
overflow: hidden;
& > .text-content {
padding: 0 30rpx 0;
font-size: 30rpx;
margin-bottom: 20rpx;
}
& .square-img {
width: 100%;
height: 200rpx;
border-radius: 6rpx;
}
& .only-img {
width: 100%;
height: 320rpx;
border-radius: 6rpx;
}
}
}
&.goods {
display: block;
& > .ui-item {
padding: 30rpx;
display: flex;
position: relative;
background-color: var(--ui-BG);
& + .ui-item {
border-top: 1rpx solid #eeeeee;
}
.content {
width: 410rpx;
padding: 0rpx;
}
.title {
font-size: 30rpx;
font-weight: 900;
color: #333333;
line-height: 1.4;
height: 1.4em;
overflow: hidden;
}
}
&.col-goods.col-twice {
display: flex;
flex-wrap: wrap;
padding-bottom: 30rpx;
& > .ui-item {
width: calc(50% - 30rpx);
margin: 20rpx 20rpx 0rpx 20rpx;
.content {
padding: 20rpx;
}
}
& > .ui-item:nth-child(2n) {
margin-left: 0rpx;
}
}
&.col-goods > .ui-item {
padding: 0rpx;
display: block;
border: 0px;
.content {
width: 100%;
padding: 30rpx;
}
}
&.no-card > .ui-item .content {
width: 470rpx;
padding: 0rpx;
}
&.no-card > .ui-item .title,
&.col-goods > .ui-item .title {
height: 3em;
overflow: hidden;
}
& > .ui-item .text-linecut-2 {
-webkit-line-clamp: 1;
}
&.no-card > .ui-item .text-linecut-2,
&.col-goods > .ui-item .text-linecut-2 {
-webkit-line-clamp: 2;
line-height: 1.6em;
height: 3.2em;
}
& > .ui-item > image {
width: 200rpx;
height: 200rpx;
margin-right: 20rpx;
border-radius: 6rpx;
}
&.no-card > .ui-item > image {
width: 220rpx;
height: 170rpx;
}
&.col-goods > .ui-item > image {
width: 100%;
height: 340rpx;
border-bottom-left-radius: 0rpx;
border-bottom-right-radius: 0rpx;
display: block;
}
&.col-goods.col-twice > .ui-item > image {
height: 236rpx;
}
}
&.loan {
display: block;
& > .ui-item {
padding: 30rpx 0 30rpx 30rpx;
display: flex;
position: relative;
background-color: var(--box-bg);
.content {
width: 450rpx;
padding: 0rpx;
.tag-list {
width: 450rpx;
display: flex;
flex-wrap: wrap;
font-size: 12px;
margin-top: 18rpx;
}
}
.action {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
}
}
&.houses {
display: block;
& > .ui-item {
padding: 20rpx;
display: flex;
position: relative;
background-color: var(--box-bg);
.image {
width: 230rpx;
height: 180rpx;
margin-right: 20rpx;
border-radius: 6rpx;
}
.content {
width: 400rpx;
padding: 0rpx;
.tag-list {
width: 400rpx;
display: flex;
flex-wrap: wrap;
font-size: 12px;
margin-top: 10rpx;
.ui-item {
height: 20px;
line-height: 20px;
}
}
}
.action {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
}
}
&.product {
display: flex;
flex-wrap: wrap;
padding-bottom: 30rpx;
& > .ui-item {
width: calc(100% - 15rpx);
margin: 20rpx 20rpx 0rpx 20rpx;
background-color: var(--box-bg);
position: relative;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
// display: flex;
// flex-wrap: wrap;
.content {
padding: 20rpx;
// width: calc(100% - 345rpx);
.text-cut {
font-size: 16px;
}
}
.image {
width: 100%;
height: 240rpx;
border-radius: 6rpx 0 0 6rpx;
display: block;
}
.ui-progress-tag {
width: 4em;
text-align: right;
font-size: 12px;
}
.border-top {
width: 100%;
}
.ui-tag {
position: absolute;
top: 0;
left: 0;
border-radius: 6rpx 0 6rpx 0;
}
}
// & > .ui-item:nth-child(2n) {
// margin-left: 0rpx;
// }
}
&.shop {
display: flex;
flex-wrap: wrap;
padding-bottom: 30rpx;
& > .ui-item {
width: calc(50% - 30rpx);
margin: 20rpx 20rpx 0rpx 20rpx;
background-color: var(--box-bg);
padding: 20rpx;
.content {
margin-top: 15rpx;
}
.image {
width: 100%;
height: 285rpx;
border-radius: 6rpx;
display: block;
}
}
& > .ui-item:nth-child(2n) {
margin-left: 0rpx;
}
}
&.orders .ui-item {
margin-top: 30rpx;
.address-box {
padding: 15rpx;
margin: 0 30rpx 30rpx;
border: 1px solid;
border-color: var(--main-a);
border-radius: 10px;
position: relative;
.ui-form-group {
min-height: 10px;
}
}
}
}

View File

@@ -0,0 +1,55 @@
.ui-code {
font-family: Monaco, Menlo, Consolas, 'Courier New';
font-size: 90%;
position: relative;
z-index: 1;
color: var(--ui-TC);
.ui-rich-text {
display: inline-block;
}
&.code {
display: inline-block;
padding: 0 10rpx;
margin: 0 10rpx;
border-radius: $radius-sm;
line-height: 1.6;
vertical-align: baseline;
}
&.pre {
display: block;
margin: 1em 0;
line-height: 1.6;
&.hasTitle {
margin: 3.2em 0 1em;
}
// border-radius: $radius-sm;
.ui-code-title {
position: absolute;
top: -2.2em;
color: var(--ui-TC-2);
left: 0;
}
.ui-rich-text {
padding: 40rpx;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
}
.ui-scroll-view {
&.ui-scroll {
max-height: 500px;
white-space: pre;
}
}
.ui-copy-btn {
position: absolute;
z-index: 2;
top: 0;
right: 0;
padding: 0.8em;
border-radius: 0 $radius-sm 0 $radius-sm;
}
}
}

View File

@@ -0,0 +1,79 @@
/* ==================
弹性布局
==================== */
.flex {
display: flex !important;
&-sub {
flex: 1 !important;
}
&-twice {
flex: 2 !important;
}
&-treble {
flex: 3 !important;
}
&-column {
flex-direction: column !important;
}
&-row {
flex-direction: row !important;
}
&-column-reverse {
flex-direction: column-reverse !important;
}
&-row-reverse {
flex-direction: row-reverse !important;
}
&-wrap {
flex-wrap: wrap !important;
}
&-center {
@include flex-center;
}
&-bar {
@include flex-bar;
}
}
.basis {
@each $class, $value in (xs: 20%, sm: 40%, df: 50%, lg: 60%, xl: 80%) {
&-#{$class} {
flex-basis: $value !important;
}
}
}
.align {
@each $class,
$value
in (start: flex-start, end: flex-end, center: center, stretch: stretch, baseline: baseline)
{
&-#{$class} {
align-items: $value !important;
}
}
}
.self {
@each $class,
$value
in (start: flex-start, end: flex-end, center: center, stretch: stretch, baseline: baseline)
{
&-#{$class} {
align-self: $value !important;
}
}
}
.justify {
@each $class,
$value
in (
start: flex-start,
end: flex-end,
center: center,
between: space-between,
around: space-around
)
{
&-#{$class} {
justify-content: $value !important;
}
}
}

121
sheep/scss/style/_form.scss Normal file
View File

@@ -0,0 +1,121 @@
/* ==================
表单
==================== */
.ui-form-item {
padding: 1rpx 24rpx;
display: flex;
align-items: center;
min-height: 100rpx;
justify-content: space-between;
.title {
text-align: justify;
padding-right: 30rpx;
font-size: 30rpx;
position: relative;
height: 60rpx;
line-height: 60rpx;
}
.content {
flex: 1;
}
input,
ui-input {
flex: 1;
font-size: 30rpx;
color: #555;
padding-right: 20rpx;
}
text[class*='icon-'] {
font-size: 36rpx;
padding: 0;
box-sizing: border-box;
}
textarea {
margin: 32rpx 0 30rpx;
height: 4.6em;
width: 100%;
line-height: 1.2em;
flex: 1;
font-size: 28rpx;
padding: 0;
}
picker,
.arrow {
flex: 1;
padding-right: 40rpx;
overflow: hidden;
position: relative;
}
picker .picker,
.arrow > view {
line-height: 100rpx;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
picker::after,
.arrow::after {
font-family: 'ui';
display: block;
content: '\e605';
position: absolute;
font-size: 34rpx;
color: #8799a3;
line-height: 100rpx;
width: 60rpx;
text-align: center;
top: 0;
bottom: 0;
right: -20rpx;
margin: auto;
}
textarea[disabled],
textarea[disabled] .placeholder {
color: transparent;
}
&.align-start .title {
height: 1em;
margin-top: 32rpx;
line-height: 1em;
}
.grid-square {
> view {
background-color: #f8f8f8;
border-radius: 12rpx;
.mask {
background-color: rgba(0, 0, 0, 0.6);
position: absolute;
font-size: 20rpx;
color: #ffffff;
width: 100%;
bottom: 0;
text-align: center;
padding: 6rpx 0;
&.red-mask {
background-color: rgba(255, 80, 80, 0.6);
}
}
[class*='icon'] {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
transform: scale(1.5);
justify-content: center;
}
.text-gray {
position: absolute;
width: 100%;
font-size: 24rpx;
text-align: center;
bottom: 20rpx;
}
}
}
}
.disabled {
opacity: 0.6;
cursor: not-allowed !important;
}

103
sheep/scss/style/_grid.scss Normal file
View File

@@ -0,0 +1,103 @@
/* ==================
栅栏
==================== */
@use 'sass:math';
@mixin make_col($screen) {
@for $i from 1 through 12 {
.ui-col-#{$screen}-#{$i} {
width: calc(100% / 12 * #{$i});
}
.ui-cols-#{$screen}-#{$i} .ui-item {
width: calc(100% / #{$i});
}
}
}
.ui-container {
box-sizing: border-box;
margin-left: auto;
margin-right: auto;
padding-left: 30rpx;
padding-right: 30rpx;
width: 100%;
max-width: 1440px;
&-fluid {
max-width: 100%;
padding-left: 0;
padding-right: 0;
}
}
.ui-grid {
display: flex;
flex-wrap: wrap;
&.multi-column {
display: block;
column-count: 2;
column-width: 0px;
column-gap: 0px;
> .ui-item {
break-inside: avoid;
padding: 0.001em;
}
}
&.grid-square {
overflow: hidden;
> .ui-item {
margin-right: 20rpx;
margin-bottom: 20rpx;
position: relative;
overflow: hidden;
}
@for $i from 1 through 12 {
&.ui-cols-#{$i} > .ui-item {
padding-bottom: calc((100% - #{20rpx * ($i - 1)}) / #{$i});
height: 0;
width: calc((100% - #{20rpx * ($i - 1)}) / #{$i});
}
}
@for $i from 1 through 12 {
&.ui-cols-#{$i} > .ui-item:nth-child(#{$i}n) {
margin-right: 0;
}
}
}
}
@for $i from 1 through 12 {
.ui-cols-#{$i} .ui-item {
width: calc(100% / #{$i});
}
}
@for $i from 1 through 12 {
.ui-col-#{$i} {
width: calc(100% / 12 * #{$i});
}
}
// 小屏
@media screen and (min-width: 0px) {
@include make_col('xs');
}
// 小屏
@media screen and (min-width: 320px) {
@include make_col('sm');
}
// 中屏
@media screen and (min-width: 768px) {
@include make_col('md');
}
// 普通屏
@media screen and (min-width: 1025px) {
@include make_col('lg');
}
// 大屏
@media screen and (min-width: 1440px) {
@include make_col('xl');
}
// 超大屏
@media screen and (min-width: 1920px) {
@include make_col('xxl');
}

View File

@@ -0,0 +1,62 @@
.cu-markdown {
position: relative;
z-index: 1;
&.selectable {
cursor: auto;
user-select: text;
}
inline {
display: inline-block;
}
.list {
.list-item {
line-height: 1.8;
.list {
margin-left: 1.28571em;
.ui-title {
transform: scale(0.6);
&:before {
content: '\e716';
}
}
}
}
.list-item-p {
position: relative;
padding-left: 1.5em;
.list-item-t {
display: block;
width: 1.3em;
text-align: center;
position: absolute;
left: 0;
}
}
}
.md-table + .md-table {
margin-top: 30rpx;
}
}
.paragraph {
margin: 0 0 40rpx;
line-height: 1.8;
}
.blockquote {
@extend .paragraph;
padding: 20rpx 30rpx;
border-left-style: solid;
border-left-width: 10rpx;
border-color: var(--ui-Border);
background: none repeat scroll 0 0 rgba(102, 128, 153, 0.05);
.paragraph {
margin-bottom: 30rpx;
}
.paragraph:last-child {
margin-bottom: 0;
}
}

View File

@@ -0,0 +1,54 @@
.ui-menu {
background-color: var(--ui-BG);
}
.ui-menu-item {
position: relative;
@include flex-bar;
min-height: 4em;
padding: 0 30rpx;
.ui-menu-item-icon {
width: 1.7em;
margin-right: 0.3em;
position: relative;
display: flex;
align-items: center;
justify-content: center;
transform: scale(1.3);
}
.ui-menu-item-icon .ui-menu-item-image {
width: 1.2em;
height: 1.2em;
display: inline-block;
}
.ui-menu-item-content {
flex: 1;
position: relative;
@include flex-bar;
}
.ui-menu-item-arrow {
width: 1.6em;
text-align: center;
color: var(--ui-TC-3);
}
&::after {
content: ' ';
width: calc(200% - 120rpx);
left: 30rpx;
position: absolute;
top: 0;
box-sizing: border-box;
height: 200%;
border-top: 1px solid var(--ui-Border);
border-radius: inherit;
transform: scale(1);
transform-origin: 0 0;
pointer-events: none;
}
&.first-item::after {
display: none;
}
&:first-child::after {
display: none;
}
}

View File

@@ -0,0 +1,90 @@
/* ==================
阴影
==================== */
.shadow {
box-shadow: var(--ui-Shadow);
&-sm {
box-shadow: var(--ui-Shadow-sm);
}
&-lg {
box-shadow: var(--ui-Shadow-lg);
}
&-inset {
box-shadow: var(--ui-Shadow-inset);
}
@each $color, $value in $colors {
@at-root .shadow-#{$color} {
box-shadow: 0 0.5em 1em rgba($value, var(--ui-Shadow-opacity));
}
&-sm.shadow-#{$color} {
box-shadow: 0 0.125em 0.25em rgba($value, var(--ui-Shadow-opacity));
}
&-lg.shadow-#{$color} {
box-shadow: 0 1em 3em rgba($value, var(--ui-Shadow-opacity-lg));
}
}
&-warp {
position: relative;
}
&-warp:before,
&-warp:after {
position: absolute;
content: '';
bottom: -10rpx;
left: 20rpx;
width: calc(50% - #{40rpx});
height: 30rpx;
transform: skew(0deg, -6deg);
transform-origin: 50% 50%;
background-color: rgba(0, 0, 0, var(--ui-Shadow-opacity));
filter: blur(20rpx);
z-index: -1;
opacity: 0.5;
}
&-warp:after {
right: 20rpx;
left: auto;
transform: skew(0deg, 6deg);
}
&-blur {
position: relative;
}
&-blur::before {
content: '';
display: block;
background: inherit;
filter: blur(20rpx);
position: absolute;
width: 100%;
height: 100%;
top: 0.5em;
left: 0.5em;
z-index: -1;
opacity: var(--ui-Shadow-opacity-lg);
transform-origin: 0 0;
border-radius: inherit;
transform: scale(1, 1);
}
}
.drop-shadow {
filter: drop-shadow(0 0 30rpx rgba(0, 0, 0, 0.1));
&-sm {
filter: drop-shadow(0 4rpx 4rpx rgba(0, 0, 0, 0.06));
}
&-lg {
filter: drop-shadow(0 30rpx 60rpx rgba(0, 0, 0, 0.2));
}
@each $color, $value in $colors {
@at-root .drop-shadow-#{$color} {
filter: drop-shadow(0 15rpx 15rpx rgba(darken($value, 10%), 0.3));
}
&-sm.drop-shadow-#{$color} {
filter: drop-shadow(0 4rpx 4rpx rgba(darken($value, 10%), 0.3));
}
&-lg.drop-shadow-#{$color} {
filter: drop-shadow(0 50rpx 100rpx rgba(darken($value, 10%), 0.2));
}
}
}

View File

@@ -0,0 +1,133 @@
.ui-table {
background-color: var(--ui-BG);
max-width: 100%;
display: table;
&.table-full {
width: 100%;
}
&.table-radius {
border-radius: $radius;
.ui-table-header {
.ui-table-tr {
border-top-left-radius: $radius;
border-top-right-radius: $radius;
}
.ui-table-th {
&:first-child {
border-top-left-radius: $radius;
}
&:last-child {
border-top-right-radius: $radius;
}
}
}
}
.ui-table-header {
display: table-header-group;
.ui-table-th {
font-weight: bold;
border-bottom: 1px solid var(--ui-Border);
white-space: nowrap;
padding: 1em 0.8em;
}
}
.ui-table-tr {
display: table-row;
z-index: 1;
}
.ui-table-body {
display: table-row-group;
position: relative;
.ui-table-tr:hover {
background-color: var(--ui-BG-1) !important;
}
.ui-table-loading {
min-height: 300px;
position: absolute !important;
width: 100%;
height: 100%;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--ui-Border);
}
}
.ui-table-td,
.ui-table-th {
display: table-cell;
text-align: unset;
padding: 0.5em 0.8em;
// font-size: 90%;
vertical-align: middle;
}
}
.ui-table.table-border {
&,
& .ui-table-td,
& .ui-table-th {
position: relative;
&::after {
content: ' ';
width: 200%;
height: 200%;
position: absolute;
top: 0;
left: 0;
border-radius: inherit;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
border: 1px solid var(--ui-Border);
z-index: 1;
}
}
.ui-table-td,
.ui-table-th {
&::after {
border-width: 1px 1px 0 0;
}
&:last-child::after {
border-right: none;
}
}
}
.ui-table.table-radius {
&::after {
border-radius: calc(#{$radius} * 2);
}
& .ui-table-tr .ui-table-th:first-child {
border-top-left-radius: calc(#{$radius} * 2);
}
& .ui-table-tr .ui-table-th:last-child {
border-top-right-radius: calc(#{$radius} * 2);
}
& .ui-table-tr:last-child .ui-table-td:first-child {
border-bottom-left-radius: #{$radius};
}
& .ui-table-tr:last-child .ui-table-td:last-child {
border-bottom-right-radius: #{$radius};
}
}
.ui-table.table-striped > .ui-table-body > .ui-table-tr:nth-child(2n + 1),
.ui-table.table-striped > .ui-table-body > .ui-table-tr:nth-child(2n + 1) {
background-color: var(--ui-BG-1);
}
.table-responsive {
width: inherit;
height: 100%;
max-width: 100%;
overflow: hidden;
box-sizing: border-box;
.table-responsive-box {
position: relative;
overflow: hidden;
}
}

View File

104
sheep/scss/style/_text.scss Normal file
View File

@@ -0,0 +1,104 @@
/* ==================
文本
==================== */
@use 'sass:math';
.font-0 {
font-size: 24rpx;
--textSize: -4rpx;
}
.font-1 {
font-size: 28rpx;
--textSize: 0rpx;
}
.font-2 {
font-size: 32rpx;
--textSize: 4rpx;
}
.font-3 {
font-size: 36rpx;
--textSize: 8rpx;
}
.font-4 {
font-size: 40rpx;
--textSize: 12rpx;
}
.text {
@each $class, $value in $fontsize {
&-#{$class},
&-#{math.div($value ,2)} {
font-size: calc(#{$value}rpx + var(--textSize)) !important;
}
}
&-cut {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@at-root [class*='text-linecut'] {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
}
@for $i from 2 through 10 {
&-linecut-#{$i} {
-webkit-line-clamp: #{$i};
}
}
&-justify {
text-align: justify;
}
&-justify-line {
text-align: justify;
line-height: 0.5em;
margin-top: 0.5em;
&::after {
content: '.';
display: inline-block;
width: 100%;
}
}
&-Abc {
text-transform: Capitalize !important;
}
&-ABC {
text-transform: Uppercase !important;
}
&-abc {
text-transform: Lowercase !important;
}
&-del,
&-line {
text-decoration: line-through !important;
}
&-bottomline {
text-decoration: underline !important;
}
&-italic {
font-style: italic !important;
}
&-style-none {
text-decoration: none !important;
}
&-break {
word-break: break-word !important;
overflow-wrap: break-word !important;
}
&-reset {
color: inherit !important;
}
&-price::before {
content: '¥';
font-size: 80%;
margin-right: 4rpx;
}
&-hide {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
}

View File

@@ -0,0 +1,58 @@
/*
* 底部导航修复样式
* 修复安全区域和层级问题
*/
/* 确保页面内容不被底部导航遮挡 */
.page-body {
padding-bottom: env(safe-area-inset-bottom);
}
/* 修复底部导航在不同设备上的显示问题 */
.u-tabbar--fixed {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 1000 !important;
background: #fff;
}
/* 确保底部导航的安全区域填充 */
.u-tabbar__content {
background: #fff;
box-shadow: 0px -2px 4px 0px rgba(51, 51, 51, 0.08);
border-top: 1rpx solid #f0f0f0;
}
/* 修复中心凸起按钮的层级问题 */
.tabbar-center-item {
z-index: 10 !important;
}
/* 修复占位元素高度 */
.u-tabbar__placeholder {
min-height: 50px;
}
/* 修复图标和文字样式 */
.u-tabbar-item__text {
font-size: 12px !important;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.u-tabbar-item__icon {
margin-bottom: 4px;
}
/* 适配 iPhone 底部安全区域 */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.u-tabbar--fixed {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@@ -0,0 +1,39 @@
// 核心主题样式文件
@mixin theme-dark {
// 背景色
--ui-BG: #393939;
--ui-BG-1: #333333;
--ui-BG-2: #2c2c2c;
--ui-BG-3: #292929;
--ui-BG-4: #222222;
// 文本色
--ui-TC: #ffffff;
--ui-TC-1: #d4d4d4;
--ui-TC-2: #919191;
--ui-TC-3: #6a6a6a;
--ui-TC-4: #474747;
// 模糊
--ui-Blur: rgba(38, 38, 38, 0.98);
--ui-Blur-1: rgba(38, 38, 38, 0.75);
--ui-Blur-2: rgba(38, 38, 38, 0.25);
--ui-Blur-3: rgba(38, 38, 38, 0.05);
// 边框
--ui-Border: rgba(119, 119, 119, 0.25);
--ui-Outline: rgba(255, 255, 255, 0.1);
--ui-Line: rgba(119, 119, 119, 0.25);
// 透明与阴影
--ui-Shadow: 0 0.5em 1em rgba(0, 0, 0, 0.45);
--ui-Shadow-sm: 0 0.125em 0.25em rgba(0, 0, 0, 0.475);
--ui-Shadow-lg: 0 1em 3em rgba(0, 0, 0, 0.475);
--ui-Shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.475);
--ui-Shadow-opacity: 0.55;
--ui-Shadow-opacity-sm: 0.175;
--ui-Shadow-opacity-lg: 0.75;
--ui-BG-opacity: 0.1;
}

View File

@@ -0,0 +1,39 @@
// 核心主题样式文件
@mixin theme-light {
// 背景色
--ui-BG: #ffffff;
--ui-BG-1: #f6f6f6;
--ui-BG-2: #f1f1f1;
--ui-BG-3: #e8e8e8;
--ui-BG-4: #e0e0e0;
// 文本色
--ui-TC: #303030;
--ui-TC-1: #525252;
--ui-TC-2: #777777;
--ui-TC-3: #9e9e9e;
--ui-TC-4: #c6c6c6;
// 模糊
--ui-Blur: rgba(255, 255, 255, 0.98);
--ui-Blur-1: rgba(255, 255, 255, 0.75);
--ui-Blur-2: rgba(255, 255, 255, 0.25);
--ui-Blur-3: rgba(255, 255, 255, 0.05);
// 边框
--ui-Border: rgba(119, 119, 119, 0.25);
--ui-Outline: rgba(0, 0, 0, 0.1);
--ui-Line: rgba(119, 119, 119, 0.25);
// 透明与阴影
--ui-Shadow: 0 0.5em 1em rgba(0, 0, 0, 0.15);
--ui-Shadow-sm: 0 0.125em 0.25em rgba(0, 0, 0, 0.075);
--ui-Shadow-lg: 0 1em 3em rgba(0, 0, 0, 0.175);
--ui-Shadow-inset: inset 0 0.1em 0.2em rgba(0, 0, 0, 0.075);
--ui-Shadow-opacity: 0.45;
--ui-Shadow-opacity-sm: 0.075;
--ui-Shadow-opacity-lg: 0.65;
--ui-BG-opacity: 0.1;
}

View File

@@ -0,0 +1,68 @@
@import './light'; //浅蓝主题
@import './dark'; //深蓝主题
// 多主题
.theme-light {
@include theme-light;
}
.theme-dark {
@include theme-dark;
}
.theme-auto {
@include theme-light;
}
@media (prefers-color-scheme: dark) {
.theme-auto {
@include theme-dark;
}
}
@each $value in ('', '-1', '-2', '-3', '-4') {
// 背景色 + 文字色 白色 + 默认色;
.ui-BG#{$value} {
background-color: var(--ui-BG#{$value}) !important;
color: var(--ui-TC);
}
// 文字颜色
.ui-TC#{$value} {
color: var(--ui-TC#{$value}) !important;
}
// 主题色背景
.ui-BG-Main#{$value} {
background-color: var(--ui-BG-Main#{$value}) !important;
color: var(--ui-BG-Main-TC) !important;
}
// 主题色渐变,横向
.ui-BG-Main-Gradient {
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient)) !important;
color: var(--ui-BG-Main-TC) !important;
}
// 主题色文字
.ui-TC-Main#{$value} {
color: var(--ui-BG-Main#{$value}) !important;
}
// 主题色阴影
.ui-Shadow-Main {
box-shadow: var(--ui-Main-box-shadow) !important;
}
.ui-BG-Main-light {
background: var(----ui-BG-Main-light) !important;
color: var(--ui-BG-Main#{$value}) !important;
}
}
@each $color, $value in $colors {
.main-#{$color} {
--ui-BG-Main: #{$value};
--ui-BG-Main-tag: #{rgba($value, 0.05)};
--ui-BG-Main-gradient: #{rgba($value, 0.6)};
--ui-BG-Main-light: #{rgba($value, 0.2)};
--ui-BG-Main-opacity-1: #{rgba($value, 0.1)};
--ui-BG-Main-opacity-4: #{rgba($value, 0.4)};
--ui-Main-box-shadow: 0 0.2em 0.5em #{rgba($value, var(--ui-Shadow-opacity))};
--ui-BG-Main-1: #{mix(rgba(255, 255, 255, 0.7), desaturate($value, 20%), 10%)};
--ui-BG-Main-2: #{mix(rgba(255, 255, 255, 0.6), desaturate($value, 40%), 20%)};
--ui-BG-Main-3: #{mix(rgba(119, 119, 119, 0.2), desaturate($value, 40%), 40%)};
--ui-BG-Main-4: #{mix(rgba(119, 119, 119, 0.1), desaturate($value, 40%), 60%)};
--ui-BG-Main-TC: #ffffff !important;
}
}

36
sheep/scss/ui.scss Normal file
View File

@@ -0,0 +1,36 @@
@import './theme/style'; //系统主题
@import './main'; //主样式*
// 确保主题色变量全局可用
:root,
page {
--ui-BG-Main: #{$primary};
--ui-BG-Main-tag: #{rgba($primary, 0.05)};
--ui-BG-Main-gradient: #{rgba($primary, 0.6)};
--ui-BG-Main-light: #{rgba($primary, 0.2)};
--ui-BG-Main-opacity-1: #{rgba($primary, 0.1)};
--ui-BG-Main-opacity-4: #{rgba($primary, 0.4)};
--ui-Main-box-shadow: 0 0.2em 0.5em #{rgba($primary, 0.45)};
--ui-BG-Main-1: #{mix(rgba(255, 255, 255, 0.7), desaturate($primary, 20%), 10%)};
--ui-BG-Main-2: #{mix(rgba(255, 255, 255, 0.6), desaturate($primary, 40%), 20%)};
--ui-BG-Main-3: #{mix(rgba(119, 119, 119, 0.2), desaturate($primary, 40%), 40%)};
--ui-BG-Main-4: #{mix(rgba(119, 119, 119, 0.1), desaturate($primary, 40%), 60%)};
--ui-BG-Main-TC: #ffffff;
}
@import './style/background'; //背景
@import './style/grid'; //列
@import './style/flex'; //布局
@import './style/border'; //边框
@import './style/text'; //文本
@import './style/shadow'; //阴影
@import './icon/style'; //图标
@import './style/tag'; //标签
@import './style/button'; //按钮
@import './style/avatar'; //头像
@import './style/table'; //表格
@import './style/code'; //代码片段
@import './style/form'; //表单
@import './style/menu'; //表单
@import './style/markdown'; //表单
@import './style/card'; //表单

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