初始化项目模板

This commit is contained in:
yangyao committed 2026-01-04 17:53:40 +08:00
1 parent 80fd6a1d4b
commit b3a69a1f27
69 files changed
+11088

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# .env.development
NODE_ENV=development
VITE_APP_RUN_ENV=development
VITE_APP_BASE_URL="http://127.0.0.1:8000"
+4
View File
@@ -0,0 +1,4 @@
# .env.production
NODE_ENV=production
VITE_APP_RUN_ENV=production
VITE_APP_BASE_URL="http://127.0.0.1:8000"
+3
View File
@@ -0,0 +1,3 @@
# Numerous always-ignore extensions
unpackage
node_modules
+26
View File
@@ -0,0 +1,26 @@
{
// launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
"version" : "0.0",
"configurations" : [
{
"app-plus" : {
"launchtype" : "local"
},
"default" : {
"launchtype" : "local"
},
"mp-alipay" : {
"launchtype" : "local"
},
"mp-weixin" : {
"launchtype" : "local"
},
"type" : "uniCloud"
},
{
"openVueDevtools" : false,
"type" : "uni-app:miniProgram"
}
]
}
+1
View File
@@ -0,0 +1 @@
uni_modules
+42
View File
@@ -0,0 +1,42 @@
// 配置几个常用的就可以
module.exports = {
// 一行最多多少个字符
printWidth: 160,
// 指定每个缩进级别的空格数
tabWidth: 2,
// 使用制表符而不是空格缩进行
useTabs: false,
// 在语句末尾打印分号
semi: true,
// 使用单引号而不是双引号
singleQuote: true,
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
quoteProps: 'as-needed',
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: false,
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
trailingComma: 'es5',
// 在对象文字中的括号之间打印空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 在jsx中把'>' 是否单独放一行
bracketSameLine: false,
// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
arrowParens: 'always',
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
rangeStart: 0,
rangeEnd: Infinity,
// 指定要使用的解析器,不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准 always\never\preserve
proseWrap: 'preserve',
// 指定HTML文件的全局空格敏感度 css\strict\ignore
htmlWhitespaceSensitivity: 'css',
// Vue文件脚本和样式标签缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
endOfLine: 'lf',
};
+178
View File
@@ -0,0 +1,178 @@
<script setup>
import {
ref
} from 'vue';
import {
onShow,
onLaunch,
onHide
} from '@dcloudio/uni-app';
import {
useI18n
} from 'vue-i18n';
import i18n from '@/lang';
import pinia from '@/store/store.js';
import {
useUserStore
} from '@/store/user';
import {
isEmpty,
platformMap
} from '@/common/common.js';
import {
getConfigInfo
} from '@/common/subscribePayment.js'
const {
t,
locale
} = useI18n();
// 初始化platform
function initApplication() {
const user = useUserStore(pinia);
// platform
let uniPlatform = uni.getSystemInfoSync().uniPlatform;
let hasPlatform = platformMap.filter((h) => h.map.match(uniPlatform))[0];
if (hasPlatform) {
// 非app,mp-weixin,mp-alipay,web都会进入这里
user.platform = hasPlatform.key;
} else {
// app
let port = uni.getSystemInfoSync().platform;
let platform = platformMap.filter((h) => h.map.match(port))[0];
user.platform = platform.key;
}
// 获取本机语种
let appLanguage = uni.getSystemInfoSync().appLanguage;
let languageMap = ['zh-CN', 'zh-TW'];
if (isEmpty(user.lang)) {
if (languageMap.indexOf(appLanguage) === -1) {
user.lang = 'zh-CN';
} else {
user.lang = appLanguage;
}
}
user.lang = user.lang.replace(/\"/g, '');
locale.value = user.lang;
i18n.global.locale.value = user.lang;
}
// 检查小程序更新
function checkUpDateMini() {
if (uni.canIUse('getUpdateManager')) {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function(res) {
if (res.hasUpdate) {
updateManager.onUpdateReady(function(ready) {
uni.showModal({
title: t('modal.upDate'),
content: t('modal.finishUpdate'),
showCancel: false,
success: function(res) {
if (res.confirm) {
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(function(err) {
uni.showModal({
title: t('modal.hasUpdate'),
content: t('modal.hasntUsed'),
showCancel: false,
});
});
}
});
} else {
uni.showModal({
title: t('modal.title'),
content: t('modal.upgradeCarrier'),
showCancel: false,
});
}
}
onLaunch(() => {
initApplication();
// getConfigInfo();
});
onShow(() => {
// #ifdef APP-PLUS
uni.onNetworkStatusChange(function(res) {
if (res.isConnected) {
setTimeout(() => {
uni.reLaunch({
url: '/pages/index/index',
});
}, 0);
} else {
uni.showModal({
cancelText: t('modal.reStart'),
confirmText: t('modal.reLoad'),
content: t('modal.offNet'),
title: t('modal.title'),
success(res) {
if (res.confirm) {
uni.reLaunch({
url: '/pages/index/index',
});
} else {
plus.runtime.restart();
}
},
});
}
});
// #endif
// #ifdef MP
checkUpDateMini();
// #endif
});
onHide(() => {});
</script>
<style lang="scss">
@import 'tailwindcss/base';
@import 'tailwindcss/utilities';
@import 'tailwindcss/components';
html,
head,
body {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: SourceHanSansCN, SourceHanSansCN;
}
page {
background: #fbfbfd;
box-sizing: border-box;
}
.shadow {
box-shadow: 0px 5rpx 10rpx 0px rgba(207, 207, 207, 0.5);
}
.border-bottom {
@apply border-0 border-b border-solid border-[#F2F2F2];
}
.border-bottom-theme {
@apply border-0 border-b border-solid border-theme;
}
.border-round-theme {
@apply border border-solid border-theme;
}
.submit-btn {
@apply bg-theme text-white h-[100rpx] text-[30rpx];
}
:root {
--theme-color: #274c40;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
import { get, post, put, dele } from '@/utils/request';
/**
* @desc 初始化支付
* groupId 订单组id
*/
export const apiInitPay = (groupId) => {
return get('/sale/tripcommon/order/pay-init',{groupId});
};
/**
* 查询客服
* @returns
*/
export const apiQueryCustomer = () => {
return get('/sale/tripcommon/index/kf');
};
/**
* 手机区号-全球
* @returns
*/
export const apiWholeAreaCode = () => {
return get('/sale/attr/tel-area-code');
};
+84
View File
@@ -0,0 +1,84 @@
import { get, post, put, dele } from '@/utils/request';
/**
* @desc 上传图片
*/
export const uploadImage = (params) => {
return post('/front/upload/image?useType=comment', params);
};
/**
* @desc 修改手机号获取验证码初始化
*/
export const editMobileInit = (params) => {
return get('/front/user/user/edit-mobile-init', params);
};
/**
* @desc 发送验证码
*/
export const sendCode = (params) => {
return get('/sale/login/send-mb-verify-code', params);
};
/**
* @desc 发送验证码初始化
*/
export const loginLRInit = (params) => {
return get('/sale/login/send-mb-verify-code-init', params);
};
/**
* @desc 登录
*/
export const login = (params) => {
return get('/sale/login/login', params);
};
/**
* @desc 注册
*/
export const register = (params) => {
return get('/front/user/login/register', params);
};
/**
* @desc 忘记密码
*/
export const forgetPassword = (params) => {
return get('/front/user/login/forget-password', params);
};
/**
* @desc 会员首页
*/
export const userIndex = (params) => {
return get('/front/user/user/index', params);
};
/**
* @desc 获取会员数据
*/
export const userInfo = (params) => {
return get('/sale/user/profile', params);
};
/**
* @desc 更改会员手机号
*/
export const updateMobile = (params) => {
return get('/sale/user/update-mobile', params);
};
/**
* @desc 更改会员资料
*/
export const updateProfile = (params) => {
return get('/sale/user/update-profile', params);
};
/**
* @desc 注销账户
*/
export const voidAccount = (params) => {
return get('/front/user/user/void-account', params);
};
/**
* @desc 获取隐私政策
*/
export const policyServer = (params) => {
return get('/sale/login/get-policy', params);
};
+1062
View File
File diff suppressed because it is too large. Load diff
+159
View File
@@ -0,0 +1,159 @@
import {
isEmpty
} from '@/common/common.js'
import {
useUserStore
} from '@/store/user.js'
import { request } from '@/utils/request.js'
/*
@desc 多端一键登录授权
@desc wx:自2023年8月26日起,手机号快速验证组件将需要付费使用。标准单价为:每次组件调用成功,收费0.03元。
zfb:用户同意授权后,scope为长期有效,需用户自行手动解除授权。
@params e 为getPhoneNumber原生按钮的回调值
*/
export function uniLogin(e) {
return new Promise((resolve, reject) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
return
}
// 获取当前操作端
uni.getProvider({
service: 'oauth',
success: (res) => {
if (res.provider.includes('weixin') || res.provider.includes('alipay')) {
// 登录
uni.login({
provider: res.provider[0],
success: (loginRes) => {
if (loginRes.code) {
// 请求后端接口解密code,拿到真实电话号码,并缓存,然后关闭弹窗,渲染页面数据
var params = {
codeType: 'mpMobile',
codeValue: JSON.stringify({
...e.detail,
...{
loginData: loginRes,
},
}),
}
request({
method: 'POST',
url: '/sale/login/login',
data: params,
}).then((resp) => {
uni.showToast({
icon: 'none',
mask: true,
title: '登录成功',
})
if (resp.success) {
const userStore = useUserStore()
userStore.userToken = resp.data.token;
userStore.isLogin = resp.data.isLogin;
userStore.setProfile({
...resp.data.userData,
});
resolve(true)
} else {
// 失败的操作
console.log('request-fail', resp)
uni.showToast({
title: '授权登录失败',
icon: 'none',
})
resolve(false)
}
})
}
},
})
}
},
fail: (err) => {
console.log({ err })
uni.showToast({
title: '授权失败',
icon: 'none',
})
},
})
})
}
/**
* @desc 获取当前小程序端的登录授权码code
*/
export function getLoginCode() {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN || MP-ALIPAY
uni.getProvider({
service: 'oauth',
success: function(res) {
if (~res.provider.indexOf('weixin') || ~res.provider.indexOf('alipay')) {
// 登录
uni.login({
provider: res.provider[0],
success: function(loginRes) {
resolve(loginRes);
},
fail(err) {
reject(err);
},
});
}
},
});
// #endif
// #ifdef MP-BAIDU
swan.getLoginCode({
success(loginRes) {
resolve(loginRes);
},
fail(err) {
reject(err);
},
});
// #endif
// #ifdef APP-PLUS || H5
resolve();
// #endif
});
}
/**
* @desc 退出登录,清除token,手机号缓存
*/
export function loginOut(status) {
const userStore = useUserStore();
userStore.clearProfile();
userStore.userToken = '';
uni.showLoading({
mask: true,
title: 'loading...'
})
setTimeout(() => {
if (status) {
uni.switchTab({
url: '/pages/index/index'
})
}
uni.hideLoading()
}, 800)
}
/**
* @desc 检查是否存在token等用户信息
* @returns false 不存在token 、true存在token
*/
export function checkLoginStatus() {
const userStore = useUserStore();
const token = userStore.userToken;
if (isEmpty(token)) {
loginOut(false)
return false
} else {
return true
}
}
+262
View File
@@ -0,0 +1,262 @@
import { apiInitPay } from '../api/common';
import { useUserStore } from '../store/user';
import { baseUrl } from '../utils/request';
import { getConfig } from '@/pages/index/api';
import i18n from '../lang';
/**
* @desc 获取全局配置参数
*/
export const getConfigInfo = () => {
getConfig().then((res) => {
if (res.success) {
const bottomNavList = res.data?.bottomNavList;
// if (bottomNavList.length > 0) {
// bottomNavList.map((item) => {
// uni.setTabBarItem({
// ...item,
// });
// });
// }
}
});
};
/**
* @desc 退款协议
* @params companyCode 退款协议需要conpanyCode
* @params isRefund false非退款协议 true退款协议
* @return html富文本
*/
export function serviceApi(companyCode, isRefund) {
return new Promise((resolve, reject) => {
let id;
if (isRefund) {
id = companyCode === 'turbojet' ? 34 : 35;
} else {
id = 1;
}
request
.post(api.infoMessage, {
id: id,
})
.then((res) => {
if (res.success) {
resolve(res.data.content);
} else {
uni.showToast({
icon: 'none',
title: res.message,
});
reject();
}
});
});
}
//
/**
* @desc APP调用第三方程序 集成微信,支付寶跳轉
*/
export function launchApp() {
if (
plus.runtime.isApplicationExist({
pname: 'com.tencent.mm',
action: 'weixin://',
}) &&
paymentKey.value.includes('wx')
) {
// 跳转应用
plus.runtime.launchApplication(
{
pname: 'com.tencent.mm',
action: 'weixin://',
},
function (e) {
console.log('e', e);
}
);
// 检查支付通道
plus.payment.getChannels((res) => {
console.log('getChannels', res);
});
// 后台获取支付参数
request
.post(api.pay, {
groupId: groupId.value,
payAgentId: payAgentId.value,
})
.then((res) => {
// 拉起微信支付
plus.payment.request(
'wxpay',
res.data,
function (resp) {
console.log('resp', resp);
},
function (err) {
console.log('err', err);
}
);
});
}
// 判断支付宝
if (
plus.runtime.isApplicationExist({
pname: 'com.eg.android.AlipayGphone',
action: 'alipay://',
}) &&
paymentKey.value.includes('zfb')
) {
plus.runtime.launchApplication(
{
pname: 'com.eg.android.AlipayGphone',
action: 'alipay://',
},
function (e) {
console.log('launchApplication', e.message);
}
);
}
}
/**
* @desc 处理支付
* @params groupId 订单组id
* @return 支付参数
*/
export const handlePay = (groupId) => {
return new Promise(async (resolve, reject) => {
const res = await apiInitPay(groupId);
if (res.success) {
console.log(res.data);
const payInfo = res.data;
// 半屏支付
if (payInfo.payType === 'mpSkip') {
uni.openEmbeddedMiniProgram({
...payInfo.mpSkipParam,
success: (res) => {
console.log('success', res);
resolve(res.data);
},
fail: (err) => {
console.log('fail', err);
resolve(err.message);
},
});
} else if (payInfo.payType === 'direct') {
handleDirectPay(groupId, () => {
resolve();
});
}
} else {
resolve(res.message);
}
});
};
export function handleDirectPay(orderNo, callback) {
const userStore = useUserStore();
let __userToken = userStore.userToken;
let __lang = userStore.lang;
let __platform = userStore.platform;
let orderNum = typeof orderNo === 'object' ? orderNo.join(',') : orderNo;
uni.showLoading({
mask: true,
title: 'loading...',
});
uni.request({
url: baseUrl + '/sale/pay/pay',
method: 'POST',
data: {
groupId: orderNum,
__token: __userToken,
__platform: __platform,
__language: __lang,
channelId: 33,
},
success: (res) => {
if (res.data.code === 0) {
let requestParams = {};
// #ifdef MP-BAIDU
requestParams = {
orderInfo: {
...res.data.data,
...res.data.data.thirdParams,
},
};
// #endif
console.log(res.data.data);
// #ifndef MP-BAIDU
requestParams = {
orderInfo: res.data.data.thirdParams ? res.data.data.thirdParams.frontData.tradeNo : '',
timeStamp: res.data.data.timeStamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: res.data.data.signType,
paySign: res.data.data.paySign,
};
// #endif
uni.requestPayment({
...requestParams,
success(res) {
// #ifdef MP-ALIPAY
if (res.resultCode === '6001') {
uni.showToast({
icon: 'none',
title: i18n.global.t('cancelPay'),
duration: 2000,
});
}
if (res.resultCode === '9000') {
uni.showToast({
icon: 'none',
title: i18n.global.t('paySuccess'),
duration: 2000,
});
}
// #endif
// #ifndef MP-ALIPAY
uni.showToast({
icon: 'none',
title: i18n.global.t('paySuccess'),
});
// #endif
},
fail(res) {
console.log('fail-res', res);
uni.showToast({
icon: 'none',
title: i18n.global.t('payFailed'),
mask: true,
});
},
complete() {
callback();
},
});
} else {
// 创建失败
uni.showToast({
icon: 'none',
title: res.data.message,
mask: true,
});
callback();
}
},
fail: (err) => {
console.log('fail-err', err);
uni.showToast({
icon: 'none',
title: err.message,
});
setTimeout(() => {
callback();
}, 1500);
},
complete() {
uni.hideLoading();
},
});
}
+166
View File
@@ -0,0 +1,166 @@
<template>
<!-- 区域号码选择 -->
<up-popup :show="isShow" round="20" closeable @close="close">
<view class="container flex flex-col px-[20px]">
<view class="title flex items-center justify-center mx-[15px]">
<text class="txt text-[16px] py-[15px] font-bold">{{ t('areaCode') }}</text>
</view>
<view class="search-box my-[10px]">
<up-search v-model="keyword" :showAction="false" :placeholder="t('areaCodePlace')" @change="handleKeywordChange"></up-search>
</view>
<!-- 主列表 -->
<view class="index-list relative overflow-hidden">
<scroll-view scroll-y :scroll-into-view="scrollIntoId" class="main-list h-[70vh] overflow-hidden" @scroll="handleScroll">
<template v-if="isEmpty(keyword)">
<view v-for="(group, index) in list" :key="index" :id="group.groupKey" class="group-container relative">
<!-- 吸顶标题 -->
<view class="group-title text-[14px] text-main py-[5px] sticky top-0 bg-white z-[999] sticky-title">
{{ group.name }}
</view>
<view v-for="(item, idx) in group.data" :key="idx" class="list-item text-main text-[14px] py-[10px]" @click="handleSelect(item)">
{{ `${item.name} (+${item.code})` }}
</view>
</view>
</template>
<view v-else v-for="(item, idx) in filterList" :key="idx" class="list-item text-main text-[14px] py-[10px]" @click="handleSelect(item)">
{{ `${item.name} (+${item.code})` }}
</view>
</scroll-view>
<!-- 右侧索引 -->
<view class="index-bar absolute top-0 right-0 py-[10px] gap-[10px] z-[9999]">
<view
v-for="(group, index) in list"
:key="index"
class="index-item text-center text-[12px] "
:class="currentIntoId === group.groupKey ? 'bg-[#e61e30] rounded-full text-white' : 'text-[#e61e30]'"
@click="handleIndexclick(group.groupKey)"
>
{{ group.groupKey }}
</view>
</view>
</view>
</view>
</up-popup>
</template>
<script setup>
import { nextTick, ref, watch } from 'vue';
import { onLoad, onReady } from '@dcloudio/uni-app';
import { useSystemInfo } from '@/hooks/useSystemInfo.js';
import { useI18n } from 'vue-i18n';
import { isEmpty } from '@/common/common.js';
import { apiWholeAreaCode } from '@/api/common.js';
const { t, locale } = useI18n();
const { computedDomHeight } = useSystemInfo();
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
// 获取区域码数据
const getAreaCodeList = async () => {
apiWholeAreaCode({ type: '' }).then((res) => {
if (res.success) {
list.value = res.data.codeGroups;
nextTick(() => {
calculateGroupPositions();
});
}
});
};
const isShow = ref(false);
watch(
() => props.show,
(newVal) => {
isShow.value = newVal;
if (newVal) getAreaCodeList();
},
{
immediate: true,
}
);
const emits = defineEmits(['close', 'select']);
const close = () => emits('close');
const keyword = ref('');
const filterList = ref([]);
const handleKeywordChange = (e) => {
if (!e) return (filterList.value = []);
const result = [];
for (let group of list.value) {
for (let item of group.data) {
const target = item.search?.replace(/\s+/g, '');
if (target.includes(e)) {
result.push(item);
}
}
}
filterList.value = uniqueByCode(result, 'code');
};
// 数组对象去重
const uniqueByCode = (arr, field) => {
const map = new Map(); // 用于存储已经出现过的 field
return arr.filter((item) => {
if (!map.has(item[field])) {
map.set(item[field], true); // 将 field 添加到 Map 中
return true; // 保留当前对象
}
return false; // 过滤掉重复对象
});
};
const handleSelect = (e) => {
emits('select', e);
};
// 数据
const list = ref([]);
const scrollIntoId = ref(''); // 当前滚动到的锚点ID
const currentIntoId = ref(''); // 当前的索引
const groupPositions = ref([]); // 存储每个组的顶部位置
// 点击索引跳转
const handleIndexclick = (title) => {
keyword.value = '';
scrollIntoId.value = title;
currentIntoId.value = title;
};
// 处理滚动事件
const handleScroll = (event) => {
const scrollTop = event.detail.scrollTop;
// 根据滚动位置判断当前吸顶的标题
for (let i = groupPositions.value.length - 1; i >= 0; i--) {
if (scrollTop > groupPositions.value[i].top) {
currentIntoId.value = groupPositions.value[i].title;
break;
}
}
};
// 计算每个组的顶部位置
const calculateGroupPositions = async () => {
let heightList = [];
for (let i = 0; i < list.value.length; i++) {
const group = list.value[i];
const data = await computedDomHeight(`#${group.groupKey}`);
if (data) {
heightList[i] = data.height || 0;
groupPositions.value.push({
title: group.groupKey,
top: i > 0 ? heightList.slice(0, i).reduce((pre, cur) => cur + pre, 0) : 0,
});
}
}
};
</script>
<style scoped lang="scss"></style>
+91
View File
@@ -0,0 +1,91 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const emit = defineEmits(['confirm', 'close']);
const props = defineProps({
title: {
// 選擇器標題
type: String,
default: () => {
return '';
},
},
show: {
// 展示狀態
type: Boolean,
default: () => {
return false;
},
},
columns: {
// 數組列表
type: Array,
default: () => {
return [];
},
},
keyName: {
type: String,
default: () => {
return 'name';
},
},
});
const showPicker = ref(false); // 是否显示弹窗
const columnList = ref([]); // 列表数据
const onConfirm = (e) => {
emit('confirm', e.value);
};
const onCancel = () => {
emit('close');
};
watch(
() => props.show,
(newVal) => {
showPicker.value = newVal;
},
{
immediate: true,
deep: true,
}
);
watch(
() => props.columns,
(newVal) => {
// console.log(newVal);
columnList.value.push(
newVal.map((item) => {
return {
...item,
};
})
);
// console.log(columnList.value);
},
{
// immediate: true,
deep: true,
}
);
</script>
<template>
<!-- <up-popup :show="showPicker" :round="10" mode="bottom" :closeOnClickOverlay="true" @close="onCancel"> -->
<up-picker
:show="showPicker"
:title="props.title"
:confirmText="t('confirm')"
:cancelText="t('cancel')"
confirmColor="#274C40"
:columns="columnList"
:keyName="keyName"
:closeOnClickOverlay="true"
@confirm="onConfirm"
@cancel="onCancel"
@close="onCancel"
></up-picker>
<!-- </up-popup> -->
</template>
<style lang="scss" scoped></style>
+99
View File
@@ -0,0 +1,99 @@
<template>
<view class="relative">
<!-- 输入框触发 -->
<input
type="text"
readonly
:value="selectedDate ? selectedDate.format('YYYY-MM-DD') : '选择日期'"
@click="showCalendar = !showCalendar"
class="w-full px-4 py-2 border-2 border-[#9C032D] rounded-lg cursor-pointer bg-white"
/>
<!-- 日历容器 -->
<view
v-if="showCalendar"
class="fixed bottom-0 left-1/2 transform -translate-x-1/2 mb-4 w-[320px] bg-[#f8d7da] border-2 border-[#9C032D] rounded-lg p-4 z-50 shadow-xl"
>
<!-- 头部 -->
<view class="flex justify-between items-center mb-4">
<button @click="changeMonth(-1)" class="text-[#9C032D] hover:text-[#80022a] transition-colors">◀</button>
<view class="font-bold text-[#9C032D]">
{{ currentDate.format('YYYY年MM月') }}
</view>
<button @click="changeMonth(1)" class="text-[#9C032D] hover:text-[#80022a] transition-colors">▶</button>
</view>
<!-- 星期 -->
<view class="grid grid-cols-7 gap-1 mb-2">
<view v-for="day in ['日', '一', '二', '三', '四', '五', '六']" :key="day" class="text-center text-sm text-[#9C032D] font-medium">
{{ day }}
</view>
</view>
<!-- 日期格子 -->
<view class="grid grid-cols-7 gap-1">
<view
v-for="(date, index) in calendarDays"
:key="index"
@click="selectDate(date)"
:class="[
'text-center p-1 rounded cursor-pointer transition-colors',
date.month() !== currentDate.month() ? 'text-gray-400' : 'text-[#9C032D]',
date.isSame(selectedDate, 'day') ? 'bg-[#9C032D] text-white' : 'hover:bg-[#f5b5ba]',
]"
>
{{ date.date() }}
</view>
</view>
<!-- 确认按钮 -->
<button @click="confirmDate" class="w-full mt-4 py-2 bg-[#9C032D] text-white rounded-lg hover:bg-[#80022a] transition-colors">确认</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
const props = defineProps({
modelValue: String, // 接收外部v-model
});
const emit = defineEmits(['update:modelValue', 'confirm']);
const showCalendar = ref(false);
const currentDate = ref(dayjs());
const selectedDate = ref(props.modelValue ? dayjs(props.modelValue) : null);
// 生成日历日期数组
const calendarDays = computed(() => {
const start = currentDate.value.startOf('month').startOf('week');
const end = currentDate.value.endOf('month').endOf('week');
const days = [];
let day = start;
while (day <= end) {
days.push(day);
day = day.add(1, 'day');
}
return days;
});
// 切换月份
const changeMonth = (delta) => {
currentDate.value = currentDate.value.add(delta, 'month');
};
// 选择日期
const selectDate = (date) => {
selectedDate.value = date;
};
// 确认选择
const confirmDate = () => {
showCalendar.value = false;
emit('update:modelValue', selectedDate.value.format('YYYY-MM-DD'));
emit('confirm', selectedDate.value.format('YYYY-MM-DD'));
};
</script>
+214
View File
@@ -0,0 +1,214 @@
<script setup>
/**
* @desc 通用布局组件
*/
import { onMounted, ref } from 'vue';
// import LoginPopup from './login-popup.vue';
import { getSystemSafeArea, getSafeTopHeight, getSystemInfo, isEmpty } from '@/common/common.js';
import { useUserStore } from '@/store/user';
import { useSystemInfo } from '@/hooks/useSystemInfo.js';
const { navHeight } = useSystemInfo();
const props = defineProps({
showBgImage: {
// 是否展示导航栏
type: Boolean,
default: () => {
return false;
},
},
showNav: {
// 是否展示导航栏
type: Boolean,
default: () => {
return true;
},
},
navTitle: {
//标题
type: String,
default: () => {
return '';
},
},
// navPlaceholder: {
// //是否生成一个等高元素,防止塌陷
// type: Boolean,
// default: () => {
// return false;
// },
// },
navShowReturn: {
//是否显示返回图标
type: Boolean,
default: () => {
return false;
},
},
navShowNavBar: {
//是否显示更多图标
type: Boolean,
default: () => {
return false;
},
},
navColor: {
//标题背景颜色
type: String,
default: () => {
return '#fff';
},
},
bgColor: {
//标题字体、icon颜色
type: String,
default: () => {
return '#fbfbfd';
},
},
titleColor: {
//标题字体、icon颜色
type: String,
default: () => {
return '#1D1D1F';
},
},
titleText: {
// 顶部标题文字
type: String,
default: () => {
return '';
},
},
// showLogin: {
// // 唤起登录
// type: Boolean,
// default: () => {
// return false;
// },
// },
});
const emit = defineEmits(['getContentHeight', 'loginStatus']);
const safeAreaInsetTop = ref(false); //是否开启顶部安全区适配
const viewHeight = ref(0); //可用高度
const pageHeight = ref(0); //页面高度
const pageWidth = ref(getSystemInfo().windowWidth);
const hasTabbar = ref(false);
const user = useUserStore();
const navPlaceholder = ref(false);
// #ifndef MP-WEIXIN
navPlaceholder.value = false;
// #endif
// #ifdef MP-WEIXIN
navPlaceholder.value = true;
// #endif
/**
* @desc 返回上一页
*/
const returnBack = () => {
uni.navigateBack(1);
};
/**
* @desc 返回内容部分的高度
*/
const contentHeight = () => {
emit('getContentHeight', {
viewHeight: viewHeight.value,
pageHeight: pageHeight.value,
});
};
const showSideNav = ref(false);
// 关闭/打开侧边弹出层
const operaSideNav = () => {
showSideNav.value = !showSideNav.value;
};
// 关闭登录弹窗
// const onCloseLogin = () => {
// const status = isEmpty(user.userToken); // true 为空,false为已登录
// emit('loginStatus', status);
// };
onMounted(() => {
// #ifndef MP-ALIPAY
safeAreaInsetTop.value = true;
// #endif
uni.showTabBar({
success() {
hasTabbar.value = true;
},
fail() {
hasTabbar.value = false;
},
complete() {
pageHeight.value = getSystemSafeArea();
if (props.showNav) {
viewHeight.value = pageHeight.value - getSafeTopHeight();
} else {
viewHeight.value = pageHeight.value;
}
contentHeight();
},
});
});
</script>
<template>
<view class="absolute left-[50%] right-0 top-0 translate-x-[-50%]" :style="{ height: `${pageHeight}px`, width: `${pageWidth}px` }">
<up-navbar
:title="props?.navTitle"
:safeAreaInsetTop="safeAreaInsetTop"
:placeholder="navPlaceholder"
:bgColor="props.navColor"
:titleStyle="{ color: props.titleColor }"
:height="navPlaceholder ? '44px' : `${getSafeTopHeight()}px`"
class="!flex-1"
v-if="props.showNav"
>
<template #left>
<view class="flex flex-row items-center gap-[10px]">
<up-icon name="list-dot" :color="props.titleColor" size="26" @click="operaSideNav" v-if="props.navShowNavBar"></up-icon>
<!-- #ifndef MP-BAIDU -->
<up-icon name="arrow-left" :color="props.titleColor" size="24" @click="returnBack" v-if="props.navShowReturn"></up-icon>
<!-- #endif -->
</view>
</template>
</up-navbar>
<view
class="relative left-0 right-0 top-0"
:style="{
height: `${viewHeight}px`,
background: props.bgColor,
top: navPlaceholder ? 0 : `${getSafeTopHeight()}px`,
}"
>
<view v-if="showBgImage" class="w-full relative">
<view
class="w-full h-[calc(200px+var(--status-bar-height))] bg-cover bg-center bg-no-repeat"
style="background-image: url('/static/test/top2.png')"
></view>
<text class="w-full text-[#fff] font-bold text-[20px] absolute left-0 right-0 bottom-[40px] text-center px-[40px]">
{{ props.titleText }}
</text>
</view>
<view
class="absolute left-0 right-0 w-full overflow-y-auto"
:style="{
top: showBgImage ? 'calc(200px + var(--status-bar-height))' : props.showNav ? 0 : `${navHeight}px`,
height: showBgImage ? `calc(100% - 200px - var(--status-bar-height))` : '100%',
}"
>
<slot name="content" :row="{ viewHeight, pageHeight }"> </slot>
</view>
<!-- 登录 -->
<!-- <LoginPopup :show="props.showLogin" @close="onCloseLogin"></LoginPopup> -->
</view>
</view>
</template>
<style lang="scss"></style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<up-popup :show="showPicker" :closeable="true" round="10" mode="bottom" @close="onClose">
<view class="login">
<view class="logo mb-[10rpx] mt-[50rpx] flex justify-center text-center">
<image class="h-[140rpx] rounded-full" src="/static/common/logo.png" mode="heightFix"></image>
</view>
<Login @close="onClose"></Login>
</view>
</up-popup>
</template>
<script setup>
import { ref, watch } from 'vue';
import Login from './login.vue';
const props = defineProps({
show: {
// 展示狀態
type: Boolean,
default: () => {
return false;
},
},
});
const emit = defineEmits(['close']);
const onClose = () => {
emit('close');
};
const showPicker = ref(false);
watch(
() => props.show,
(newVal) => {
showPicker.value = newVal;
},
{
immediate: true,
deep: true,
}
);
</script>
<style lang="scss" scoped></style>
+855
View File
@@ -0,0 +1,855 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { areaCodeOptions, checkTelPhone, checkEmail, createV, showLoadingToast, closeToast, showToast, isEmpty } from '@/common/common.js';
import { getLoginCode } from '@/common/login.js';
import { login, register, sendCode, policyServer, loginLRInit } from '@/api/user.js';
import { useUserStore } from '@/store/user';
import { useI18n } from 'vue-i18n';
import CommonPicker from './common-picker.vue';
const { t, locale } = useI18n();
const userStore = useUserStore();
const isLogin = ref(true); // 是否登录,默认登录,为false表示注册
import { uniLogin } from '@/common/login';
const isMobile = ref(true); //是否手机号登录
const isPassword = ref(false); //是否密码登录
const isForgetPassword = ref(false); //是否忘记密码
// const isDisable = ref(true); //是否登录注册按钮是否禁用,即是否通过校验
const mobile = ref(''); // 手机号
const areaCode = ref(''); // 地区码
const code = ref(''); // 验证码
const password = ref(''); // 密码
const passwordConfirm = ref(''); // 确认密码
const email = ref(''); // 邮箱
const emit = defineEmits(['close']);
const wxJsCode = ref({}); //wx授权码code
const codeType = ref(''); // 区分code信息类型:=mpLogin(小程序中手机号+验证码登录);=mpMobile(小程序中授权获取手机号登录)
//同意条款相关
const checked = ref(false);
const onChange = (e) => {
checked.value = !checked.value;
};
//地区码弹框相关
const columns = ref([]);
const showCommonPicker = ref(false);
const onConfirmCommonPicker = (data) => {
areaCode.value = data[0].value;
showCommonPicker.value = false;
};
const onCloseCommonPicker = () => {
showCommonPicker.value = false;
};
//导航手机号点击功能
const onMobile = () => {
isMobile.value = true;
password.value = '';
passwordConfirm.value = '';
// isDisable.value = true;
// console.log(isMobile.value);
};
//导航邮箱点击功能
const onEmail = () => {
isMobile.value = false;
isPassword.value = true;
password.value = '';
passwordConfirm.value = '';
// isDisable.value = true;
};
//去登录
const toLogin = () => {
isLogin.value = true;
isMobile.value = true;
isPassword.value = true;
mobile.value = '';
areaCode.value = '852';
code.value = '';
password.value = '';
passwordConfirm.value = '';
email.value = '';
clearInterval(timerId2.value);
};
//去注册
const toRegister = () => {
isLogin.value = false;
isMobile.value = true;
isPassword.value = true;
mobile.value = '';
areaCode.value = '852';
code.value = '';
password.value = '';
passwordConfirm.value = '';
email.value = '';
clearInterval(timerId1.value);
};
/**
* @desc 登录
*/
const onLogin = () => {
if (!checked.value) {
uni.showToast({
title: t('policyTips'),
icon: 'none',
duration: 2000,
});
return;
}
//登录参数
let params = {};
//小程序免验证登录,codeType.value = mpMobile时,表示本次登录为免验证登录,noVerify值为1
if (codeType.value == 'mpMobile') {
params.codeValue = JSON.stringify(wxJsCode.value); //微信端需要传递登录授权code
params.noVerify = 1; // 免验证登录必传, codeType.value = mpMobile 时,为1表示本次登录为免验证登录
} else {
//手机号+验证码登录
params.codeType = userStore.platform == 'h5' ? '' : codeType.value; // 小程序有效,仅在短信验证码登录时有值 mpLogin
params.loginType = userStore.platform == 'h5' ? 'mobile' : ''; // h5环境下短信验证码登录必须要
params.areaCode = isMobile.value ? areaCode.value : ''; // 区号
params.mobile = isMobile.value ? mobile.value : ''; // 手机号
params.verifyCode = code.value; // 验证码
// #ifdef MP-WEIXIN || MP-ALIPAY
params.codeValue = JSON.stringify(wxJsCode.value); //微信端需要传递登录授权code
// #endif
//登录前的验证
if (!LoginCheck()) {
return;
}
}
console.log(params);
try {
uni.showLoading({
mask: true,
});
login(params).then((res) => {
console.log(res);
uni.hideLoading();
if (res.success) {
userStore.userToken = res.data.token;
userStore.isLogin = res.data.isLogin;
userStore.setProfile({
...res.data.userData,
});
emit('close');
mobile.value = '';
password.value = '';
code.value = '';
email.value = '';
} else {
//重新获取code
// #ifdef MP-WEIXIN || MP-ALIPAY
wxGetLoginCode();
// #endif
uni.showToast({
title: res.message,
icon: 'none',
});
}
});
} catch (error) {
//重新获取code
// #ifdef MP-WEIXIN || MP-ALIPAY
wxGetLoginCode();
// #endif
uni.showToast({
title: error.message,
icon: 'none',
});
} finally {
}
};
//登录校验
const LoginCheck = (val) => {
if (isMobile.value) {
//手机号登录
if (!checkTelPhone(areaCode.value, mobile.value)) {
uni.showToast({
title: t('personal.mobileTips'),
icon: 'none',
});
return false;
}
if (isPassword.value) {
//密码登录
if (password.value == '' || password.value == null) {
uni.showToast({
title: '请输入密码',
icon: 'none',
});
return false;
}
} else {
//验证码登录
if (code.value == '' || code.value == null) {
uni.showToast({
title: t('personal.validatePlaceholder'),
icon: 'none',
});
return false;
}
}
} else {
//邮箱登录
if (!checkEmail(email.value)) {
uni.showToast({
title: '请输入正确的电子邮箱',
icon: 'none',
});
return false;
}
if (password.value == '' || password.value == null) {
uni.showToast({
title: '请输入密码',
icon: 'none',
});
return false;
}
}
return true;
};
//注册
const onRegister = () => {
if (!checked.value) {
uni.showToast({
title: t('policyTips'),
icon: 'none',
duration: 2000,
});
return;
}
let params = {
regType: isMobile.value ? '1' : '2', //1手機號 2郵箱
areaCode: isMobile.value ? areaCode.value : '',
mobile: isMobile.value ? mobile.value : '',
password: isPassword.value ? password.value : '',
passwordConfirm: isPassword.value ? passwordConfirm.value : '',
code: isMobile.value ? code.value : '',
email: !isMobile.value ? email.value : '',
// #ifdef MP-WEIXIN || MP-ALIPAY
wxJsCode: wxJsCode.value, //微信端需要传递登录授权code
// #endif
};
console.log(params);
if (!RegisterCheck()) {
return;
}
try {
uni.showLoading({
mask: true,
});
//注册api
register(params).then((res) => {
uni.hideLoading();
if (res.success) {
userStore.userToken = res.data.token;
userStore.setProfile({
...res.data.user,
});
emit('close');
mobile.value = '';
password.value = '';
passwordConfirm.value = '';
code.value = '';
email.value = '';
} else {
//重新获取code
// #ifdef MP-WEIXIN || MP-ALIPAY
wxGetLoginCode();
// #endif
uni.showToast({
title: res.message,
icon: 'none',
});
}
});
} catch (error) {
//重新获取code
// #ifdef MP-WEIXIN || MP-ALIPAY
wxGetLoginCode();
// #endif
uni.showToast({
title: error.message,
icon: 'none',
});
} finally {
}
};
//注册校验
const RegisterCheck = (val) => {
if (isMobile.value) {
//手机号注册
if (!checkTelPhone(areaCode.value, mobile.value)) {
uni.showToast({
title: '请输入正确的地区号和手机号',
icon: 'none',
});
return false;
}
if (code.value == '' || code.value == null) {
uni.showToast({
title: '请输入验证码',
icon: 'none',
});
return false;
}
} else {
//邮箱注册
if (!checkEmail(email.value)) {
uni.showToast({
title: '请输入正确的电子邮箱',
icon: 'none',
});
return false;
}
}
if (password.value == '' || password.value == null) {
uni.showToast({
title: '请输入密码',
icon: 'none',
});
return false;
} else {
if (password.value?.length < 6 || password.value?.length > 20) {
uni.showToast({
title: '密碼長度應為6-20位',
icon: 'none',
});
return false;
}
}
if (passwordConfirm.value == '' || passwordConfirm.value == null) {
uni.showToast({
title: '請再次輸入密碼',
icon: 'none',
});
return false;
} else {
if (password.value != passwordConfirm.value) {
uni.showToast({
title: '兩次密碼不一樣',
icon: 'none',
});
return false;
}
}
return true;
};
const dataV = ref(''); //加密数据
const isCanWxLogin = ref(false); // 允许国内公司快速获取手机号登录
/**
* 初始化,获取data.v
*/
const initMobile = async () => {
try {
await loginLRInit().then((res) => {
if (res.success) {
areaCode.value = res.data.areaCodeList[0].value;
isCanWxLogin.value = res.data.isAllowMpMobile; // 允许国内公司快速获取手机号登录
res.data.areaCodeList.forEach((item, index) => {
columns.value[index] = {
value: item.value,
name: item.label,
};
});
dataV.value = res.data.v;
console.log(dataV.value);
} else {
uni.showToast({
title: res.message,
icon: 'none',
});
}
});
} catch (error) {
uni.showToast({
title: error.message,
icon: 'none',
});
}
};
const timerId1 = ref(null); // 定时器1(登录)
const timerId2 = ref(null); // 定时器2(注册)
const countDown1 = ref(60); // 倒计时1(登录)
const allowSendCode1 = ref(true); // 允许发送验证码1(登录)
const countDown2 = ref(60); // 倒计时2(注册)
const allowSendCode2 = ref(true); // 允许发送验证码2(注册)
//获取验证码
const getValifyCode = () => {
//手机号注册
if (!checkTelPhone(areaCode.value, mobile.value)) {
uni.showToast({
title: t('personal.mobileTips'),
icon: 'none',
});
return false;
}
let account = areaCode.value + ',' + mobile.value;
//加密data.v
let postV;
if (isLogin.value) {
postV = createV(dataV.value, account);
} else {
postV = createV(dataV.value, account);
}
let params = {
// account: isMobile.value ? account : '', //1手機號 2郵箱
// type: isMobile.value ? '1' : '2', //類型:1 手機號 2 郵箱
// purpose: !isLogin.value ? '1' : !isForgetPassword.value ? '2' : '3', //目的:1註冊 2登錄 3忘記密碼
areaCode: areaCode.value,
mobile: mobile.value,
v: postV, //加密数据
};
try {
// console.log(params);
uni.showLoading({
mask: true,
});
sendCode(params).then((res) => {
uni.hideLoading();
if (res.success) {
uni.showToast({
title: t('personal.resendSuccess'),
icon: 'none',
});
// #ifdef MP
codeType.value = 'mpLogin';
// #endif
if (isLogin.value) {
allowSendCode1.value = false;
timerId1.value = setInterval(() => {
countDown1.value--;
if (countDown1.value == 0) {
countDown1.value = 60;
allowSendCode1.value = true;
clearInterval(timerId1.value);
// console.log(timerId1.value);
}
}, 1000);
} else {
allowSendCode2.value = false;
timerId2.value = setInterval(() => {
countDown2.value--;
if (countDown2.value == 0) {
countDown2.value = 60;
allowSendCode2.value = true;
clearInterval(timerId2.value);
}
}, 1000);
}
} else {
uni.showToast({
title: res.message,
icon: 'none',
});
}
});
} catch (error) {
uni.showToast({
title: error.message,
icon: 'none',
});
}
};
//忘记密码
const toForgetPassword = () => {
uni.navigateTo({
url: '/pages/user/forgetPassword',
});
};
// 协议弹框相关
const noticeData = ref({}); //协议数据
const showAgreementPicker = ref(false);
const onShowAgreementPicker = () => {
showAgreementPicker.value = true;
};
const onCancelAgreementPicker = () => {
showAgreementPicker.value = false;
};
const getPolicyServer = async () => {
try {
await policyServer({
type: '',
}).then((res) => {
console.log(res);
if (res.success) {
noticeData.value = res.data?.content;
} else {
uni.showToast({
title: res.message,
icon: 'none',
});
}
});
} catch (error) {
uni.showToast({
title: error.message,
icon: 'none',
});
}
};
/**
* @desc 获取微信登录授权码
*/
const wxGetLoginCode = async () => {
await getLoginCode().then((res) => {
wxJsCode.value = res;
console.log(wxJsCode.value, '新获取的code');
});
};
/**
* @desc 免验证登录
*/
const quickLogin = () => {
getLoginCode().then((res) => {
// #ifdef MP
codeType.value = 'mpMobile';
// #endif
wxJsCode.value = res;
onLogin();
});
};
// 获取用户手机号码
const onGetphonenumber = (ev) => {
uni.$u.debounce(async () => {
if (ev.detail.errMsg.includes('fail')) return;
if (!checked.value) {
uni.showToast({
title: t('policyTips'),
icon: 'none',
duration: 2000,
});
return;
}
try {
uni.showLoading({
title: t('loading'),
mask: true,
});
const flag = await uniLogin(ev);
if (!flag) {
uni.showToast({
title: '登录失败',
icon: 'none',
});
} else {
emit('close');
console.log({
flag,
});
}
} catch (err) {
console.log({
err,
});
} finally {
uni.hideLoading();
}
}, 1000);
};
const handleChecked = () => {
if (!checked.value) {
uni.showToast({
title: t('policyTips'),
icon: 'none',
duration: 2000,
});
return;
}
};
onMounted(async () => {
await initMobile();
// await getPolicyServer();
// #ifdef MP-WEIXIN || MP-ALIPAY
await wxGetLoginCode();
// #endif
});
onUnmounted(() => {
clearInterval(timerId1.value);
clearInterval(timerId2.value);
});
</script>
<template>
<!-- <view class="login-type flex text-[34rpx] text-theme">
<view class="mobile-type flex-1 font-medium border-bottom" @tap="onMobile" :class="{ border-bottom-theme: isMobile }">{{ isLogin ? '手機號碼登入' : '手機號碼註冊' }}</view>
<view class="email-type flex-1 font-medium border-bottom" @tap="onEmail" :class="{ border-bottom-theme: !isMobile }">{{ isLogin ? '電郵登入' : '電郵註冊' }}</view>
</view> -->
<!-- 微信快捷登录 -->
<view v-if="isLogin && isCanWxLogin">
<view class="login-box">
<button
v-if="!checked"
class="up-reset-button bg-theme rounded-full mx-[30rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white"
@click="handleChecked"
>
{{ t('phoneQuickLogin') }}
</button>
<button
v-else
class="up-reset-button bg-theme rounded-full mx-[30rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white"
open-type="getPhoneNumber"
@getphonenumber="onGetphonenumber"
>
{{ t('phoneQuickLogin') }}
</button>
</view>
</view>
<!-- 登录 -->
<view v-else-if="isLogin">
<view class="mobile" v-if="isMobile">
<view class="label text-[26rpx]">{{ t('personal.mobile') }}</view>
<view class="mobile-text flex items-center border-round-theme">
<view class="areaCode flex items-center">
<view class="text-[30rpx]" @tap="showCommonPicker = true">+{{ areaCode }}</view>
<view class="down-icon">
<up-icon name="arrow-down-fill" size="8" color></up-icon>
</view>
</view>
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none" />
<!-- #endif -->
</view>
</view>
<view class="email" v-else>
<view class="label text-[26rpx]">{{ t('personal.email') }}</view>
<view class="email-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none" />
<!-- #endif -->
</view>
</view>
<view class="password" v-if="isPassword">
<view class="label text-[26rpx]">{{ t('personal.password') }}</view>
<view class="password-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.passwordPlaceholder')" v-model="password" type="password" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.passwordPlaceholder')" v-model="password" type="password" border="none" />
<!-- #endif -->
</view>
</view>
<view class="code" v-else>
<view class="label text-[26rpx]">{{ t('personal.validateCode') }}</view>
<view class="code-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none" />
<!-- #endif -->
<view v-if="allowSendCode1" class="whitespace-nowrap text-[28rpx] font-medium text-theme" @tap="getValifyCode">{{
t('personal.getValidateCode')
}}</view>
<view v-else class="whitespace-nowrap text-[28rpx] font-medium text-[rgb(131,131,131)]"> {{ countDown1 }}s{{ t('personal.resend') }} </view>
</view>
</view>
<!-- 免验证登录 -->
<!-- #ifdef MP -->
<view class="flex flex-row items-center justify-center flex-1" @click="quickLogin">
<text class="text-[15px]">{{ t('quickLogin') }}</text>
</view>
<!-- #endif -->
<!-- <view class="text-right text-[26rpx] mx-[30rpx] text-gray-600" @tap="toForgetPassword">忘記密碼?</view> -->
<view class="bg-theme rounded-full mx-[30rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white" @tap="onLogin">{{
t('login')
}}</view>
<!-- <view class="flex mx-[30rpx] items-center justify-between text-[28rpx]">
<view>沒有賬號?點挈 <text class="text-theme" @tap="toRegister">註冊</text></view>
<view v-if="isMobile" class="text-theme" @tap="isPassword = !isPassword">{{ !isPassword ? '密碼登入' : '验证码登入' }}</view>
</view> -->
</view>
<!-- 注册 -->
<view v-else>
<view class="mobile" v-if="isMobile">
<view class="label text-[26rpx]">{{ t('personal.mobile') }}</view>
<view class="mobile-text flex items-center border-round-theme">
<view class="areaCode flex items-center">
<view class="text-[30rpx]" @tap="showCommonPicker = true">+{{ areaCode }}</view>
<view class="down-icon">
<up-icon name="arrow-down-fill" size="8" color></up-icon>
</view>
</view>
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.mobilePlaceholder')" v-model="mobile" type="number" border="none" />
<!-- #endif -->
</view>
</view>
<view class="email" v-else>
<view class="label text-[26rpx]">{{ t('personal.email') }}</view>
<view class="email-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.emailPlaceholder')" v-model="email" border="none" />
<!-- #endif -->
</view>
</view>
<view class="code" v-if="isMobile">
<view class="label text-[26rpx]">{{ t('personal.validateCode') }}</view>
<view class="code-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" placeholder="t('personal.validatePlaceholder')" v-model="code" type="number" border="none" />
<!-- #endif -->
<view v-if="allowSendCode2" class="whitespace-nowrap text-[28rpx] font-medium text-theme" @tap="getValifyCode">{{
t('personal.getValidateCode')
}}</view>
<view v-else class="whitespace-nowrap text-[28rpx] font-medium text-[rgb(131,131,131)]">
{{ `${countDown2}s ${t('personal.resend')}` }}
</view>
</view>
</view>
<view class="password">
<view class="label text-[26rpx]">{{ t('personal.password') }}</view>
<view class="password-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.setPassword')" v-model="password" type="password" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.setPassword')" v-model="password" type="password" border="none" />
<!-- #endif -->
</view>
</view>
<view class="password">
<view class="label text-[26rpx]">{{ t('personal.confirmPassword') }}</view>
<view class="password-text flex items-center border-round-theme">
<!-- #ifndef MP-ALIPAY -->
<up-input :placeholder="t('personal.passwordAgain')" v-model="passwordConfirm" type="password" border="none"></up-input>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<input class="flex-1" :placeholder="t('personal.passwordAgain')" v-model="passwordConfirm" type="password" border="none" />
<!-- #endif -->
</view>
</view>
<!-- :class="{ disable: isDisable }" -->
<view
class="bg-theme rounded-full mx-[30rpx] mt-[15rpx] my-[40rpx] flex h-[92rpx] items-center justify-center text-[34rpx] font-medium text-white"
@tap="onRegister"
>{{ t('regLogin') }}</view
>
<view class="flex mx-[30rpx] mb-[40rpx] items-center justify-between text-[28rpx]">
<view
>{{ t('hasAccount') }} <text class="text-theme" @tap="toLogin">{{ t('login') }}</text></view
>
</view>
</view>
<view class="text-center my-[40rpx] text-[28rpx]">
<checkbox-group @change="onChange">
<checkbox class="checkbox" :checked="checked" color="#00A88E"> </checkbox>
{{ t('policyNote') }} <text class="text-theme font-medium" @tap="onShowAgreementPicker">{{ t('policy') }}</text>
</checkbox-group>
</view>
<!-- 地区号 -->
<CommonPicker
:title="t('personal.selectAreaCode')"
:show="showCommonPicker"
:columns="columns"
@confirm="onConfirmCommonPicker"
@close="onCloseCommonPicker"
></CommonPicker>
<!-- 用户协议弹框 -->
<up-popup :show="showAgreementPicker" :closeable="true" round="10" mode="bottom" @close="onCancelAgreementPicker">
<view class="h-[70vh] flex flex-col px-[30rpx] pb-[30rpx] text-[28rpx]">
<view class="py-[30rpx] text-center text-[36rpx]">
{{ t('policy') }}
</view>
<view class="flex-1 overflow-y-scroll">
<up-parse :content="noticeData"></up-parse>
</view>
</view>
</up-popup>
</template>
<style lang="scss" scoped>
.border-bottom-theme .login-type {
height: 88rpx;
margin: 30rpx;
.mobile-type,
.email-type {
text-align: center;
line-height: 88rpx;
}
}
.mobile,
.email,
.password,
.code {
margin: 30rpx;
.label {
font-size: 26rpx;
padding-left: 40rpx;
padding-bottom: 20rpx;
}
.mobile-text,
.email-text,
.password-text,
.code-text {
// margin: 0 15px;
height: 100rpx;
padding: 0 40rpx;
border-radius: 100rpx;
.van-field {
background-color: #ffffff !important;
padding: 0 10rpx !important;
}
.areaCode {
margin-right: 17rpx;
.down-icon {
width: 40rpx;
padding-left: 10rpx;
}
border-right: 2rpx solid #eee;
}
}
}
.disable {
opacity: 0.3;
}
</style>
+135
View File
@@ -0,0 +1,135 @@
<script setup>
import {
getPredictTime
} from '@/pages/booking/api.js';
import {
onMounted,
ref,
reactive,
watch
} from 'vue';
import {
useI18n
} from 'vue-i18n';
import {
getNowDate
} from '@/common/common';
import {
isEmpty,
showToast
} from '../common/common';
const {
t,
locale
} = useI18n();
const props = defineProps({
show: {
type: Boolean,
default: false,
},
date: {
type: String,
default: getNowDate(),
},
});
const emit = defineEmits(['close', 'confirm']);
const close = () => {
emit('close');
};
const pckList = ref([]); // 套餐总数组
const title = ref('');
/**
* @desc 選擇套餐
* @param res 選中的套餐
*/
const choiceValue = ref('');
const choiceData = ref({});
const choice = (res) => {
if (!res.time.status) {
return showToast(res.time.tips);
}
choiceData.value = {
pckId: res.item.pckId,
pckName: res.item.pckName,
...res.time,
};
choiceValue.value = res.time.value;
};
const confirm = () => {
if (isEmpty(choiceData.value)) {
return showToast(t('booking.useTimePlaceholder'));
}
emit('confirm', choiceData.value);
close();
};
/**
* @desc 查询用餐时间
*/
const init = () => {
return new Promise((resolve, reject) => {
getPredictTime({
date: props.date,
}).then((res) => {
if (res.success) {
pckList.value = res.data?.pckList;
title.value = res.data?.title;
resolve()
} else {
showToast(res.message)
reject()
}
});
})
}
const timeFun = reactive({
init
});
defineExpose({
timeFun,
});
onMounted(() => {
init()
});
</script>
<template>
<up-popup :show="props.show" mode="bottom" round="20" :closeable="true" :safeAreaInsetBottom="true" @close="close">
<view class="flex flex-col gap-[20px] p-[20px] h-[70vh] relative">
<view class="flex h-[44px] items-center justify-center text-[16px] text-[#1D1D1F]">{{ title }}</view>
<view class="flex flex-col overflow-auto time-height gap-[20px]">
<view class="flex flex-col gap-[20px]" v-for="item in pckList" :key="item.pckId">
<view class="flex flex-col gap-[10px]">
<view class="text-main text-[14px] font-bold">{{ item.pckName }}</view>
<view class="gap-[20px] grid grid-cols-2">
<view class="flex items-center justify-center h-[35px] rounded-[5px] text-[14px]"
v-for="time in item.useTimeData" :key="time.value" :class="{
'!bg-theme text-white': choiceValue === time.value,
'bg-[#999]/[.5] text-white': !time.status,
'bg-white text-main border-round-theme': time.status,
}" @click="choice({ item, time })">
{{ time.label }}
</view>
</view>
</view>
</view>
</view>
<view class="my-[20px] flex absolute bottom-0 left-[20px] right-[20px]">
<up-button class="h-[40px] flex-1" :text="t('confirm')" shape="circle" color="#B5966E"
@click="confirm" />
</view>
</view>
</up-popup>
</template>
<style scoped lang="scss">
.time-height {
height: calc(100% - 40px - 44px - 40px); // 按钮高度+title高度+内边距上下共40px
}
</style>
+128
View File
@@ -0,0 +1,128 @@
export default {
theme: {
colors: {
/**
* 项目主题颜色,背景色系、字体色系、边框色系均有用到
* 用法: 背景色系(bg-theme)
* 字体色系(text-theme)
* 边框色系(border-theme)
* 页面:首页/pages/index/index、产品列表页/pages/product/index、客服页/pages/customer/index、个人中心/pages/mine/index
*/
theme: '#00A88E',
/**
* 用法: 浅背景主题色(bg-theme-opacity)
* 页面: 产品列表页/pages/product/index
*/
'theme-opacity': '#ECF8EA',
tab: '#303030', //导航栏字体颜色
main: '#333333', //黑色字体颜色
white: '#fff', //白色字体
/**
* 用法: 备注类字体颜色(text-describe)
* 页面: 产品列表页/pages/product/index
*/
describe: '#666666',
/**
* 用法: 价格字体颜色(text-price)
* 页面: 订单详情页/pages/order/order-detail、订单列表页/pages/order/index
*/
price: '#D4493C',
/**
* 用法: 价格后缀字体颜色(text-price-after)
* 页面: 产品详情页/pages/product/detail
*/
'price-after': '#7F8186',
/**
* 用法: 倒计时字体颜色(text-countdown)
* 页面: 订单详情页/pages/order/order-detail、订单列表页/pages/order/index
*/
countdown: '#DB1D0C',
/**
* 用法: 未支付字体颜色(text-order-unpay)
* 页面: 订单详情页/pages/order/order-detail
*/
'order-unpay': '#DB1D0C',
/**
* 用法: 未支付字体颜色(text-order-usable)
* 页面: 订单详情页/pages/order/order-detail
*/
'order-usable': '#274C40',
/**
* 用法: 已完成字体颜色(text-order-finish)
* 页面: 订单详情页/pages/order/order-detail
*/
'order-finish': '#666666',
/**
* 主要边框颜色(用的最多)
* 用法:(border-bg-theme)
* 页面:产品列表页/pages/product/index
*/
'bd-main': '#D1B043',
/**
* 产品详情页-tab栏背景色
* 页面:产品详情页/pages/product/detail
*/
'product-detail-tabs-bg': '#f4f6f5',
/**
* 产品详情页-tab栏字体颜色
* 页面:产品详情页/pages/product/detail
*/
'product-detail-tabs-text': '#252527',
/**
* 产品详情页-套餐背景色
* 页面:产品详情页/pages/product/detail
*/
productPckBg: '#f9f9f9',
/**
* 評價tab背景色
* 页面:产品详情页/pages/product/detail
*/
commentTabBg: '#eaedec',
/**
* 評價tab背景色-選中
* 页面:产品详情页/pages/product/detail
*/
commentTabBgActive: '#fff',
/**
* 評價tab字體顏色
* 页面:产品详情页/pages/product/detail
*/
commentTabText: '#333333',
/**
* 評價tab字體顏色-選中
* 页面:产品详情页/pages/product/detail
*/
commentTabTextActive: '#274C40',
/**
* 預訂彈窗日期字體顏色
* 页面:产品详情页/pages/product/detail
*/
bookingPopupDateText: '#222',
/**
* 預訂彈窗日期字體顏色-選中
* 页面:产品详情页/pages/product/detail
*/
bookingPopupDateTextActive: '#222',
/**
* 預訂彈窗日期邊框顏色
* 页面:产品详情页/pages/product/detail
*/
bookingPopupDateBorderColor: 'rgba(39, 76, 64, 0.40)',
/**
* 預訂彈窗日期邊框顏色-選中
* 页面:产品详情页/pages/product/detail
*/
bookingPopupDateBorderColorActive: '#274C40',
/**
* 預訂彈窗日期背景顏色
* 页面:产品详情页/pages/product/detail
*/
bookingPopupDateBgColor: '#fff',
/**
* 預訂彈窗日期背景顏色-選中
* 页面:产品详情页/pages/product/detail
*/
bookingPopupDateBgColorActive: '#eaedec',
},
},
};
+29
View File
@@ -0,0 +1,29 @@
import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
export function useSharePage(getParams) {
const info = uni.getSystemInfoSync();
// 页面中仍需要导入onShareAppMessage
onShareAppMessage((e) => {
if (info.uniPlatform !== 'web' && info.uniPlatform !== 'app') {
uni.showShareMenu({
withShareTicket: true,
menu: ['shareAppMessage', 'shareTimeline'],
});
}
const params = getParams || {}; // 每次分享时获取最新的参数
const buttonParams = e.target?.dataset || {};
return {
title: '',
...params,
...buttonParams,
};
});
// 页面中仍需要导入onShareTimeline
onShareTimeline(() => {
const params = getParams || {}; // 每次分享时获取最新的参数
return {
title: '',
...params,
};
});
}
+104
View File
@@ -0,0 +1,104 @@
import {
ref,
onMounted,
getCurrentInstance
} from 'vue'
export function useSystemInfo() {
const {
proxy
} = getCurrentInstance()
const systemInfo = ref({})
const validHeight = ref(0)
const navHeight = ref(0)
// 获取页面可用高度
const getValidHeight = (info) => {
const windowHeight = info.windowHeight
// 在非tabbar页,ios端预留底部安全高度
const safeBottom = info.osName === 'ios' && info.safeAreaInsets.bottom && info.uniPlatform !== 'web' && info
.uniPlatform !== 'app' ? 20 : 0
return windowHeight - safeBottom
}
// 获取导航栏高度
const getNavHeight = (info) => {
let navHeight = 0
if (info.uniPlatform === 'mp-weixin') {
const custom = uni.getMenuButtonBoundingClientRect();
navHeight = custom.height + (custom.top - info.statusBarHeight) * 2;
} else if (info.uniPlatform === 'mp-alipay') {
navHeight = info.titleBarHeight
} else if (info.uniPlatform === 'mp-baidu') {
navHeight = info.navigationBarHeight
}
return navHeight
}
onMounted(() => {
systemInfo.value = uni.getSystemInfoSync()
validHeight.value = getValidHeight(systemInfo.value)
navHeight.value = getNavHeight(systemInfo.value)
})
// 计算dom高度(需要在mounted生命周期后调用)
const computedDomHeight = (selectors, selectType = 'select') => {
return new Promise(async (resolve, reject) => {
let context = uni.createSelectorQuery().in(proxy)
if (systemInfo.value.uniPlatform === 'mp-alipay') {
context = uni.createSelectorQuery();
}
let domHeight = 0;
let domWidth = 0;
let queriesCount = 0;
let selectorQueries = [];
// 如果selectors是字符串,则将其转换为单元素数组
if (typeof selectors === 'string') {
selectors = [selectors];
}
if (!Array.isArray(selectors)) {
console.error('selectors 必须是字符串或字符串数组')
reject(false)
return
}
// 长度为0
if (!selectors.length) {
resolve(0)
return
}
// 添加到执行上下文
for (let selectorStr of selectors) {
context[selectType](selectorStr).boundingClientRect();
}
// 执行查询
context.exec(result => {
// 累加高度
result.forEach((cur) => {
if (cur) {
domHeight += cur.height;
domWidth += cur.width;
}
});
resolve({
height: domHeight,
width: domWidth,
rect: result
});
});
})
}
return {
systemInfo,
validHeight,
navHeight,
computedDomHeight
}
}
+8
View File
@@ -0,0 +1,8 @@
/**
* 判断是否为移动端
* @returns {Boolean} true为移动端
*/
export function useUsingMobile() {
const isUsingMobile = /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent);
return isUsingMobile;
}
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<link rel="icon" href="./static/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/animate.css@3.5.2/animate.min.css" />
<title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
import {
createI18n
} from 'vue-i18n'
import zhCn from './zh-cn.js'
import zhTw from './zh-tw.js'
const messages = {
'zh-CN': zhCn,
'zh-TW': zhTw,
}
const i18n = createI18n({
legacy: false, // 使用CompotitionAPI必须添加这条.
globalInjection: true,
locale: 'zh-TW',
messages
})
export default i18n
+5
View File
@@ -0,0 +1,5 @@
export default {
appName: '跳跳糖',
};
+4
View File
@@ -0,0 +1,4 @@
export default {
appName: 'sweekar',
};
+83
View File
@@ -0,0 +1,83 @@
import App from './App'
import {
nextTick
} from "vue"
// 全局注册layout组件
import Layout from '@/components/layout.vue'
// 引入pinia
import pinia from "@/store/store.js"
// 引入uvie-plus
import uviewPlus from 'uview-plus'
// 引入封装好的request
import {
request
} from '@/utils/request.js';
// 引入vue-i18n多语言翻译
import i18n from '@/lang/index.js'
// 引入animate.css 具体查看 https://www.dowebok.com/demo/2014/98/
import 'animate.css';
/* 非app断网提示 */
// #ifndef APP-PLUS
uni.onNetworkStatusChange((res) => {
if (res.networkType === null || res.networkType === "unknown" || (!res.isConnected)) {
uni.showModal({
cancelText: i18n.global.t('modal.reStart'),
confirmText: i18n.global.t('modal.reLoad'),
content: i18n.global.t('modal.offNet'),
title: i18n.global.t('modal.title'),
success(res) {
if (res.confirm) {
uni.navigateBack(0)
} else {
uni.reLaunch({
url: '/pages/index/index'
})
}
}
})
}
})
// #endif
// Vue2
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// Vue3
// #ifdef VUE3
import {
createSSRApp
} from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.component('Layout', Layout).use(uviewPlus).use(i18n).use(pinia)
// 必须使用 nextTick,不然会有加载顺序问题,导致绑定失败
nextTick(() => {
// 代替 Vue.prototype.$xxx 绑定
app.config.globalProperties.$request = request; // 自定义添加request
// 全局异常拦截
app.config.errorHandler = (err, instance, info) => {
console.log(err, instance, info);
uni.hideLoading()
uni.showToast({
icon: 'none',
title: '❌ ' + err.message,
duration: 3000
})
}
})
return {
app
}
}
// #endif
+99
View File
@@ -0,0 +1,99 @@
{
"name" : "sweekar-app",
"appid" : "__UNI__AB3EDCE",
"description" : "",
"versionName" : "1.1.3",
"versionCode" : 113,
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* 模块配置 */
"modules" : {},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
"minSdkVersion" : 21,
"targetSdkVersion" : 34
},
/* ios打包配置 */
"ios" : {
"idfa" : false
},
/* SDK配置 */
"sdkConfigs" : {}
},
"nvueLaunchMode" : ""
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false,
"minified" : true
},
"usingComponents" : true,
"lazyCodeLoading" : "requiredComponents"
},
"mp-alipay" : {
"usingComponents" : true,
"appid" : "2021004158656274"
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"fallbackLocale" : "zh-Hant",
"locale" : "zh-Hant",
"h5" : {
"router" : {
"mode" : "hash",
"base" : "./"
}
},
"proxy" : {
"/sale" : {
"target" : "http://110.v2-app.devcn.dabapiao.com",
"changeOrigin" : true,
"secure" : false,
"pathRewrite" : {
"^/sale" : "/"
}
}
}
}
+4151
View File
File diff suppressed because it is too large. Load diff
+30
View File
@@ -0,0 +1,30 @@
{
"dependencies": {
"animate.css": "^4.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.9",
"pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0",
"pinyin-pro": "^3.26.0",
"uview-plus": "^3.4.20",
"vue": "^3.3.4",
"vue-i18n": "^9.3.0-beta.25",
"zipson": "^0.2.12"
},
"devDependencies": {
"prettier": "^3.3.3",
"sass": "^1.65.1",
"sass-loader": "^10.4.1",
"tailwindcss": "^3.4.17",
"weapp-tailwindcss": "^3.7.0"
},
"scripts": {
"postinstall": "weapp-tw patch"
},
"name": "quick-mp-front",
"version": "1.0.0",
"main": "main.js",
"author": "",
"license": "ISC",
"description": ""
}
+81
View File
@@ -0,0 +1,81 @@
{
"easycom": {
// 注意一定要放在custom里,否则无效,https://ask.dcloud.net.cn/question/131175
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/order/index",
"style": {
"navigationStyle": "custom",
// #ifdef MP-ALIPAY
"enablePullDownRefresh": true
// #endif
}
},
{
"path": "pages/mine/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/product/index",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "跳跳糖",
"navigationBarBackgroundColor": "#fff",
"backgroundColor": "#F2F3F9"
},
"uniIdRouter": {},
"tabBar": {
"color": "#999999",
"selectedColor": "#AB002D",
"custom": true,
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "/static/tabbar/home.png",
"selectedIconPath": "/static/tabbar/active-home.png"
},
{
"pagePath": "pages/order/index",
"iconPath": "/static/tabbar/order.png",
"selectedIconPath": "/static/tabbar/active-order.png"
},
{
"pagePath": "pages/mine/index",
"iconPath": "/static/tabbar/mine.png",
"selectedIconPath": "/static/tabbar/active-mine.png"
}
]
}
}
+15
View File
@@ -0,0 +1,15 @@
import { get, post, put, dele } from '@/utils/request';
/**
* @desc 全局参数
*/
export const getConfig = (params) => {
return get('/sale/tripcommon/index/config', params);
};
/**
* @desc 获取产品列表
*/
export const getProducts = (params) => {
return get('/api/products/', params)
}
+109
View File
@@ -0,0 +1,109 @@
<script setup>
import { ref, onMounted, getCurrentInstance, nextTick } from 'vue';
import { onLoad, onShow, onReady, onHide, onPullDownRefresh } from '@dcloudio/uni-app';
import { getSystemSafeArea, getSystemInfo, isEmpty, showToast, showLoadingToast, closeToast } from '@/common/common.js';
import LoginPopup from '@/components/login-popup.vue';
import { getProducts } from './api.js';
import { useUserStore } from '@/store/user.js';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const props = defineProps(['status']);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const tabList = ref([]); //tab列表
const tabsIndex = ref(0); //当前选中的tab索引
// const modelTypeList = ref([]);
const tabsLeft = ref(0); //下划线移动的距离
const finished = ref(false); //数据是否全部加载完成
const windowWidth = ref(0); //tab元素的宽度
const orderRows = ref([]); //订单列表
const showLogin = ref(false); //显示登录
const triggered = ref(false); //是否下拉刷新状态,true为是
const loading = ref(false); // 添加loading状态
const productData = ref();
const scrollHeight = ref(0); //scroll-view滚动高度
const pageView = ref({});
/**
* @desc 获取可视内容高度
*/
const getContentHeight = (e) => {
pageView.value = e;
};
/**
* @desc 跳转至详情页面
*/
const toDetailPage = (productId) => {
uni.navigateTo({
url: '/pages/product/detail',
});
};
//获取首页数据
const getProductList = async()=>{
const res = await getProducts();
productData.value=res.data;
console.log("商品数据",productData.value);
}
onLoad(()=>{
// getProductList();
});
// 模拟数据
/**
* @desc 处理预订事件
*/
const handleBook = (productId) => {
// 这里可以添加预订逻辑,比如跳转到详情页
uni.navigateTo({
url: `/pages/product/detail?id=${productId}`,
});
};
</script>
<template>
<view class="h-[140rpx]">hello</view>
</template>
<style scoped lang="scss">
:deep(.u-count-down__text) {
color: #ab002d !important;
font-size: 13px !important;
}
.navigation {
border-bottom: 1px solid #f2f2f2;
}
.navi-title-center {
left: 50%;
transform: translateX(-50%);
}
.line {
transition: 0.3s all;
}
.search {
:deep(.u-search) {
flex: 1;
}
}
</style>
+7
View File
@@ -0,0 +1,7 @@
import { get, post, put, dele } from '@/utils/request';
/**
* @desc 获取产品列表
*/
export const getProducts = (params) => {
return get('/api/products/', params)
}
+13
View File
@@ -0,0 +1,13 @@
<template>
<view>
</view>
</template>
<script setup>
</script>
<style>
</style>
+7
View File
@@ -0,0 +1,7 @@
import { get, post, put, dele } from '@/utils/request';
/**
* @desc 获取产品列表
*/
export const getProducts = (params) => {
return get('/api/products/', params)
}
+109
View File
@@ -0,0 +1,109 @@
<script setup>
import { ref, onMounted, getCurrentInstance, nextTick } from 'vue';
import { onLoad, onShow, onReady, onHide, onPullDownRefresh } from '@dcloudio/uni-app';
import { getSystemSafeArea, getSystemInfo, isEmpty, showToast, showLoadingToast, closeToast } from '@/common/common.js';
import LoginPopup from '@/components/login-popup.vue';
import { getProducts } from './api.js';
import { useUserStore } from '@/store/user.js';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const props = defineProps(['status']);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const tabList = ref([]); //tab列表
const tabsIndex = ref(0); //当前选中的tab索引
// const modelTypeList = ref([]);
const tabsLeft = ref(0); //下划线移动的距离
const finished = ref(false); //数据是否全部加载完成
const windowWidth = ref(0); //tab元素的宽度
const orderRows = ref([]); //订单列表
const showLogin = ref(false); //显示登录
const triggered = ref(false); //是否下拉刷新状态,true为是
const loading = ref(false); // 添加loading状态
const productData = ref();
const scrollHeight = ref(0); //scroll-view滚动高度
const pageView = ref({});
/**
* @desc 获取可视内容高度
*/
const getContentHeight = (e) => {
pageView.value = e;
};
/**
* @desc 跳转至详情页面
*/
const toDetailPage = (productId) => {
uni.navigateTo({
url: '/pages/product/detail',
});
};
//获取首页数据
const getProductList = async()=>{
const res = await getProducts();
productData.value=res.data;
console.log("商品数据",productData.value);
}
onLoad(()=>{
// getProductList();
});
// 模拟数据
/**
* @desc 处理预订事件
*/
const handleBook = (productId) => {
// 这里可以添加预订逻辑,比如跳转到详情页
uni.navigateTo({
url: `/pages/product/detail?id=${productId}`,
});
};
</script>
<template>
<view class="h-[140rpx]">hello</view>
</template>
<style scoped lang="scss">
:deep(.u-count-down__text) {
color: #ab002d !important;
font-size: 13px !important;
}
.navigation {
border-bottom: 1px solid #f2f2f2;
}
.navi-title-center {
left: 50%;
transform: translateX(-50%);
}
.line {
transition: 0.3s all;
}
.search {
:deep(.u-search) {
flex: 1;
}
}
</style>
+7
View File
@@ -0,0 +1,7 @@
import { get, post, put, dele } from '@/utils/request';
/**
* @desc 获取产品列表
*/
export const getProducts = (params) => {
return get('/api/products/', params)
}
+128
View File
@@ -0,0 +1,128 @@
<script setup>
import { ref, onMounted, getCurrentInstance, nextTick } from 'vue';
import { onLoad, onShow, onReady, onHide, onPullDownRefresh } from '@dcloudio/uni-app';
import { getSystemSafeArea, getSystemInfo, isEmpty, showToast, showLoadingToast, closeToast } from '@/common/common.js';
import LoginPopup from '@/components/login-popup.vue';
import { getProducts } from './api.js';
import { useUserStore } from '@/store/user.js';
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const props = defineProps(['status']);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const tabList = ref([]); //tab列表
const tabsIndex = ref(0); //当前选中的tab索引
// const modelTypeList = ref([]);
const tabsLeft = ref(0); //下划线移动的距离
const finished = ref(false); //数据是否全部加载完成
const windowWidth = ref(0); //tab元素的宽度
const orderRows = ref([]); //订单列表
const showLogin = ref(false); //显示登录
const triggered = ref(false); //是否下拉刷新状态,true为是
const loading = ref(false); // 添加loading状态
const productData = ref();
const scrollHeight = ref(0); //scroll-view滚动高度
const pageView = ref({});
/**
* @desc 获取可视内容高度
*/
const getContentHeight = (e) => {
pageView.value = e;
};
/**
* @desc 跳转至详情页面
*/
const toDetailPage = (productId) => {
uni.navigateTo({
url: '/pages/product/detail',
});
};
//获取首页数据
const getProductList = async()=>{
const res = await getProducts();
productData.value=res.data;
console.log("商品数据",productData.value);
}
onLoad(()=>{
// getProductList();
});
// 模拟数据
/**
* @desc 处理预订事件
*/
const handleBook = (productId) => {
// 这里可以添加预订逻辑,比如跳转到详情页
uni.navigateTo({
url: `/pages/product/detail?id=${productId}`,
});
};
</script>
<template>
<view class="overflow-hidden" :style="{ height: `${pageView.pageHeight}px` }">
<Layout :navShowNavBar="false" bgColor="#F7F7F7" @getContentHeight="getContentHeight">
<template #content>
<view class="w-full flex flex-col gap-[15px] p-[15px]">
<view class="flex p-[10px] gap-[15px] border border-round-theme rounded-[10px]" v-for="(product,index) in productData" :key="index">
<image :src="product.image" class="w-[80px] h-[80px] rounded-[10px]"></image>
<view class="flex-1 flex flex-col gap-[4px]">
<text class="text-[15px] text-[#333333] font-bold">{{product.name}}</text>
<text class="text-[12px] text-[#1D9EF8] font-Medium">{{product.description}}</text>
<view class="flex gap-[10px]">
<text class="text-price">售价:{{product.price}}</text>
</view>
</view>
</view>
</view>
</template>
</Layout>
</view>
</template>
<style scoped lang="scss">
:deep(.u-count-down__text) {
color: #ab002d !important;
font-size: 13px !important;
}
.navigation {
border-bottom: 1px solid #f2f2f2;
}
.navi-title-center {
left: 50%;
transform: translateX(-50%);
}
.line {
transition: 0.3s all;
}
.search {
:deep(.u-search) {
flex: 1;
}
}
</style>
+30
View File
@@ -0,0 +1,30 @@
{
"appid": "wx64689b2e9eecaff7",
"projectname": "hktourbus",
"compileType": "miniprogram",
"libVersion": "3.8.0",
"packOptions": {
"ignore": [],
"include": []
},
"condition": {},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"compileHotReLoad": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

+83
View File
@@ -0,0 +1,83 @@
import {
ref,
reactive
} from 'vue';
import {
defineStore
} from 'pinia'
import {
parse,
stringify
} from 'zipson'
export const useCommon = defineStore(
'common',
() => {
const test = 'test'
return {
test
}
}, {
persist: {
paths: ['commonState'],
storage: {
getItem(key) {
return uni.getStorageSync(key)
},
setItem(key, value) {
uni.setStorageSync(key, value)
},
},
serializer: {
deserialize: parse,
serialize: stringify,
}
}
})
/*
页面内使用pinia
1、引用你定义的store实例
import { useUserStore } from '@/stores/user'
2、实例化useUserStore
const user = useUserStore()
3、引入 storeToRefs 用于 给实例的属性创建refs,即响应式
import { storeToRefs } from 'pinia'
例如:const {count} = storeToRefs(user)
count.value 此时为响应式
4、修改state 方式共两种 如下
user.count++
4-1、
// 带自动补全 ✨
user.$patch({ count: counter.count + 1 })
$patch()可批量修改state的属性值
例如:
批量修改
const pathState = () => {
store.$patch({
user: { name: 'ks', age: 20 },
});
};
其他操作state的方法如下:
替换State
const replaceState = () => {
store.$state.user = { age: 20, name: 'eee', sex: 'women' };
};
重置state的值
const reset = () => {
store.$reset();
};
4-2、
// 或使用 action 代替
user.increment()
*/
+7
View File
@@ -0,0 +1,7 @@
import {
createPinia
} from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
export default pinia;
+112
View File
@@ -0,0 +1,112 @@
// 引用pinia商店
import { ref, reactive } from 'vue';
import { defineStore } from 'pinia';
import { parse, stringify } from 'zipson';
export const useUserStore = defineStore(
'user',
() => {
const userInfo = ref({});
const platform = ref('');
const userToken = ref('');
const isLogin = ref(false);
const currency = ref('HKD');
const kfTelCN = ref('');
const kfTelHK = ref('');
const wxLink = ref('');
const lang = ref('');
const setProfile = (val) => {
userInfo.value = {
...val,
};
console.log(userInfo.value);
};
//清理用户信息,退出时使用
const clearProfile = () => {
userInfo.value = {
areaCode: '',
avatar: '',
birthday: null,
email: null,
gender: null,
mobile: '',
nickName: '',
trueName: null,
userId: null,
};
};
return {
isLogin,
userInfo,
userToken,
platform,
currency,
kfTelCN,
kfTelHK,
wxLink,
lang,
setProfile,
clearProfile,
};
},
{
persist: {
storage: {
getItem(key) {
return uni.getStorageSync(key);
},
setItem(key, value) {
uni.setStorageSync(key, value);
},
},
serializer: {
deserialize: parse,
serialize: stringify,
},
},
}
);
/*
页面内使用pinia
1、引用你定义的store实例
import { useUserStore } from '@/stores/user'
2、实例化useUserStore
const user = useUserStore()
3、引入 storeToRefs 用于 给实例的属性创建refs,即响应式
import { storeToRefs } from 'pinia'
例如:const {count} = storeToRefs(user)
count.value 此时为响应式
4、修改state 方式共两种 如下
user.count++
4-1、
// 带自动补全 ✨
user.$patch({ count: counter.count + 1 })
$patch()可批量修改state的属性值
例如:
批量修改
const pathState = () => {
store.$patch({
user: { name: 'ks', age: 20 },
});
};
其他操作state的方法如下:
替换State
const replaceState = () => {
store.$state.user = { age: 20, name: 'eee', sex: 'women' };
};
重置state的值
const reset = () => {
store.$reset();
};
4-2、
// 或使用 action 代替
user.increment()
*/
+27
View File
@@ -0,0 +1,27 @@
const path = require('path');
const config = require('./config');
const resolve = (p) => {
return path.resolve(__dirname, p);
};
/** @type {import('tailwindcss').Config} */
module.exports = {
// 增加前辍避免样式冲突
// prefix: 'zhs-',
// 注意此处,一定要 `path.resolve` 一下, 传入绝对路径
// 你要有其他目录,比如 components,也必须在这里,添加一下
theme: {
extend: {
colors: {
'custom-color': '#9C032D',
},
},
},
variants: {},
plugins: [],
content: ['./pages/**/*.{html,js,ts,jsx,tsx,vue}', './components/**/*.{html,js,ts,jsx,tsx,vue}'].map(resolve),
corePlugins: {
// 跨多端可以 h5 开启,小程序关闭
preflight: false,
},
...config,
};
+77
View File
@@ -0,0 +1,77 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* uview-plus */
@import 'uview-plus/theme.scss';
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;
+30
View File
@@ -0,0 +1,30 @@
## 1.4.12(2024-09-21)
- 修复 calendar在选择日期范围后重新选择日期需要点两次的Bug
## 1.4.11(2024-01-10)
- 修复 回到今天时,月份显示不一致问题
## 1.4.10(2023-04-10)
- 修复 某些情况 monthSwitch 未触发的Bug
## 1.4.9(2023-02-02)
- 修复 某些情况切换月份错误的Bug
## 1.4.8(2023-01-30)
- 修复 某些情况切换月份错误的Bug [详情](https://ask.dcloud.net.cn/question/161964)
## 1.4.7(2022-09-16)
- 优化 支持使用 uni-scss 控制主题色
## 1.4.6(2022-09-08)
- 修复 表头年月切换,导致改变当前日期为选择月1号,且未触发change事件的Bug
## 1.4.5(2022-02-25)
- 修复 条件编译 nvue 不支持的 css 样式的Bug
## 1.4.4(2022-02-25)
- 修复 条件编译 nvue 不支持的 css 样式的Bug
## 1.4.3(2021-09-22)
- 修复 startDate、 endDate 属性失效的Bug
## 1.4.2(2021-08-24)
- 新增 支持国际化
## 1.4.1(2021-08-05)
- 修复 弹出层被 tabbar 遮盖的Bug
## 1.4.0(2021-07-30)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.3.16(2021-05-12)
- 新增 组件示例地址
## 1.3.15(2021-02-04)
- 调整为uni_modules目录规范
@@ -0,0 +1,544 @@
/**
* @1900-2100区间内的公历、农历互转
* @charset UTF-8
* @github https://github.com/jjonline/calendar.js
* @Author Jea杨(JJonline@JJonline.Cn)
* @Time 2014-7-21
* @Time 2016-8-13 Fixed 2033hex、Attribution Annals
* @Time 2016-9-25 Fixed lunar LeapMonth Param Bug
* @Time 2017-7-24 Fixed use getTerm Func Param Error.use solar year,NOT lunar year
* @Version 1.0.3
* @公历转农历:calendar.solar2lunar(1987,11,01); //[you can ignore params of prefix 0]
* @农历转公历:calendar.lunar2solar(1987,09,10); //[you can ignore params of prefix 0]
*/
/* eslint-disable */
var calendar = {
/**
* 农历1900-2100的润大小信息表
* @Array Of Property
* @return Hex
*/
lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049
/** Add By JJonline@JJonline.Cn**/
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, // 2050-2059
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, // 2060-2069
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, // 2070-2079
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, // 2080-2089
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, // 2090-2099
0x0d520], // 2100
/**
* 公历每个月份的天数普通表
* @Array Of Property
* @return Number
*/
solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
/**
* 天干地支之天干速查表
* @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"]
* @return Cn string
*/
Gan: ['\u7532', '\u4e59', '\u4e19', '\u4e01', '\u620a', '\u5df1', '\u5e9a', '\u8f9b', '\u58ec', '\u7678'],
/**
* 天干地支之地支速查表
* @Array Of Property
* @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"]
* @return Cn string
*/
Zhi: ['\u5b50', '\u4e11', '\u5bc5', '\u536f', '\u8fb0', '\u5df3', '\u5348', '\u672a', '\u7533', '\u9149', '\u620c', '\u4ea5'],
/**
* 天干地支之地支速查表<=>生肖
* @Array Of Property
* @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"]
* @return Cn string
*/
Animals: ['\u9f20', '\u725b', '\u864e', '\u5154', '\u9f99', '\u86c7', '\u9a6c', '\u7f8a', '\u7334', '\u9e21', '\u72d7', '\u732a'],
/**
* 24节气速查表
* @Array Of Property
* @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"]
* @return Cn string
*/
solarTerm: ['\u5c0f\u5bd2', '\u5927\u5bd2', '\u7acb\u6625', '\u96e8\u6c34', '\u60ca\u86f0', '\u6625\u5206', '\u6e05\u660e', '\u8c37\u96e8', '\u7acb\u590f', '\u5c0f\u6ee1', '\u8292\u79cd', '\u590f\u81f3', '\u5c0f\u6691', '\u5927\u6691', '\u7acb\u79cb', '\u5904\u6691', '\u767d\u9732', '\u79cb\u5206', '\u5bd2\u9732', '\u971c\u964d', '\u7acb\u51ac', '\u5c0f\u96ea', '\u5927\u96ea', '\u51ac\u81f3'],
/**
* 1900-2100各年的24节气日期速查表
* @Array Of Property
* @return 0x string For splice
*/
sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
'97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
'97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
'7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
'977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
'665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'],
/**
* 数字转中文速查表
* @Array Of Property
* @trans ['日','一','二','三','四','五','六','七','八','九','十']
* @return Cn string
*/
nStr1: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d', '\u5341'],
/**
* 日期转农历称呼速查表
* @Array Of Property
* @trans ['初','十','廿','卅']
* @return Cn string
*/
nStr2: ['\u521d', '\u5341', '\u5eff', '\u5345'],
/**
* 月份转农历称呼速查表
* @Array Of Property
* @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊']
* @return Cn string
*/
nStr3: ['\u6b63', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d', '\u5341', '\u51ac', '\u814a'],
/**
* 返回农历y年一整年的总天数
* @param lunar Year
* @return Number
* @eg:var count = calendar.lYearDays(1987) ;//count=387
*/
lYearDays: function (y) {
var i; var sum = 348
for (i = 0x8000; i > 0x8; i >>= 1) { sum += (this.lunarInfo[y - 1900] & i) ? 1 : 0 }
return (sum + this.leapDays(y))
},
/**
* 返回农历y年闰月是哪个月;若y年没有闰月 则返回0
* @param lunar Year
* @return Number (0-12)
* @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6
*/
leapMonth: function (y) { // 闰字编码 \u95f0
return (this.lunarInfo[y - 1900] & 0xf)
},
/**
* 返回农历y年闰月的天数 若该年没有闰月则返回0
* @param lunar Year
* @return Number (0、29、30)
* @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29
*/
leapDays: function (y) {
if (this.leapMonth(y)) {
return ((this.lunarInfo[y - 1900] & 0x10000) ? 30 : 29)
}
return (0)
},
/**
* 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法
* @param lunar Year
* @return Number (-1、29、30)
* @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29
*/
monthDays: function (y, m) {
if (m > 12 || m < 1) { return -1 }// 月份参数从1至12,参数错误返回-1
return ((this.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29)
},
/**
* 返回公历(!)y年m月的天数
* @param solar Year
* @return Number (-1、28、29、30、31)
* @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30
*/
solarDays: function (y, m) {
if (m > 12 || m < 1) { return -1 } // 若参数错误 返回-1
var ms = m - 1
if (ms == 1) { // 2月份的闰平规律测算后确认返回28或29
return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28)
} else {
return (this.solarMonth[ms])
}
},
/**
* 农历年份转换为干支纪年
* @param lYear 农历年的年份数
* @return Cn string
*/
toGanZhiYear: function (lYear) {
var ganKey = (lYear - 3) % 10
var zhiKey = (lYear - 3) % 12
if (ganKey == 0) ganKey = 10// 如果余数为0则为最后一个天干
if (zhiKey == 0) zhiKey = 12// 如果余数为0则为最后一个地支
return this.Gan[ganKey - 1] + this.Zhi[zhiKey - 1]
},
/**
* 公历月、日判断所属星座
* @param cMonth [description]
* @param cDay [description]
* @return Cn string
*/
toAstro: function (cMonth, cDay) {
var s = '\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf'
var arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22]
return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + '\u5ea7'// 座
},
/**
* 传入offset偏移量返回干支
* @param offset 相对甲子的偏移量
* @return Cn string
*/
toGanZhi: function (offset) {
return this.Gan[offset % 10] + this.Zhi[offset % 12]
},
/**
* 传入公历(!)y年获得该年第n个节气的公历日期
* @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起
* @return day Number
* @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春
*/
getTerm: function (y, n) {
if (y < 1900 || y > 2100) { return -1 }
if (n < 1 || n > 24) { return -1 }
var _table = this.sTermInfo[y - 1900]
var _info = [
parseInt('0x' + _table.substr(0, 5)).toString(),
parseInt('0x' + _table.substr(5, 5)).toString(),
parseInt('0x' + _table.substr(10, 5)).toString(),
parseInt('0x' + _table.substr(15, 5)).toString(),
parseInt('0x' + _table.substr(20, 5)).toString(),
parseInt('0x' + _table.substr(25, 5)).toString()
]
var _calday = [
_info[0].substr(0, 1),
_info[0].substr(1, 2),
_info[0].substr(3, 1),
_info[0].substr(4, 2),
_info[1].substr(0, 1),
_info[1].substr(1, 2),
_info[1].substr(3, 1),
_info[1].substr(4, 2),
_info[2].substr(0, 1),
_info[2].substr(1, 2),
_info[2].substr(3, 1),
_info[2].substr(4, 2),
_info[3].substr(0, 1),
_info[3].substr(1, 2),
_info[3].substr(3, 1),
_info[3].substr(4, 2),
_info[4].substr(0, 1),
_info[4].substr(1, 2),
_info[4].substr(3, 1),
_info[4].substr(4, 2),
_info[5].substr(0, 1),
_info[5].substr(1, 2),
_info[5].substr(3, 1),
_info[5].substr(4, 2)
]
return parseInt(_calday[n - 1])
},
/**
* 传入农历数字月份返回汉语通俗表示法
* @param lunar month
* @return Cn string
* @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月'
*/
toChinaMonth: function (m) { // 月 => \u6708
if (m > 12 || m < 1) { return -1 } // 若参数错误 返回-1
var s = this.nStr3[m - 1]
s += '\u6708'// 加上月字
return s
},
/**
* 传入农历日期数字返回汉字表示法
* @param lunar day
* @return Cn string
* @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一'
*/
toChinaDay: function (d) { // 日 => \u65e5
var s
switch (d) {
case 10:
s = '\u521d\u5341'; break
case 20:
s = '\u4e8c\u5341'; break
case 30:
s = '\u4e09\u5341'; break
default :
s = this.nStr2[Math.floor(d / 10)]
s += this.nStr1[d % 10]
}
return (s)
},
/**
* 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春”
* @param y year
* @return Cn string
* @eg:var animal = calendar.getAnimal(1987) ;//animal='兔'
*/
getAnimal: function (y) {
return this.Animals[(y - 4) % 12]
},
/**
* 传入阳历年月日获得详细的公历、农历object信息 <=>JSON
* @param y solar year
* @param m solar month
* @param d solar day
* @return JSON object
* @eg:console.log(calendar.solar2lunar(1987,11,01));
*/
solar2lunar: function (y, m, d) { // 参数区间1900.1.31~2100.12.31
// 年份限定、上限
if (y < 1900 || y > 2100) {
return -1// undefined转换为数字变为NaN
}
// 公历传参最下限
if (y == 1900 && m == 1 && d < 31) {
return -1
}
// 未传参 获得当天
if (!y) {
var objDate = new Date()
} else {
var objDate = new Date(y, parseInt(m) - 1, d)
}
var i; var leap = 0; var temp = 0
// 修正ymd参数
var y = objDate.getFullYear()
var m = objDate.getMonth() + 1
var d = objDate.getDate()
var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000
for (i = 1900; i < 2101 && offset > 0; i++) {
temp = this.lYearDays(i)
offset -= temp
}
if (offset < 0) {
offset += temp; i--
}
// 是否今天
var isTodayObj = new Date()
var isToday = false
if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
isToday = true
}
// 星期几
var nWeek = objDate.getDay()
var cWeek = this.nStr1[nWeek]
// 数字表示周几顺应天朝周一开始的惯例
if (nWeek == 0) {
nWeek = 7
}
// 农历年
var year = i
var leap = this.leapMonth(i) // 闰哪个月
var isLeap = false
// 效验闰月
for (i = 1; i < 13 && offset > 0; i++) {
// 闰月
if (leap > 0 && i == (leap + 1) && isLeap == false) {
--i
isLeap = true; temp = this.leapDays(year) // 计算农历闰月天数
} else {
temp = this.monthDays(year, i)// 计算农历普通月天数
}
// 解除闰月
if (isLeap == true && i == (leap + 1)) { isLeap = false }
offset -= temp
}
// 闰月导致数组下标重叠取反
if (offset == 0 && leap > 0 && i == leap + 1) {
if (isLeap) {
isLeap = false
} else {
isLeap = true; --i
}
}
if (offset < 0) {
offset += temp; --i
}
// 农历月
var month = i
// 农历日
var day = offset + 1
// 天干地支处理
var sm = m - 1
var gzY = this.toGanZhiYear(year)
// 当月的两个节气
// bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year`
var firstNode = this.getTerm(y, (m * 2 - 1))// 返回当月「节」为几日开始
var secondNode = this.getTerm(y, (m * 2))// 返回当月「节」为几日开始
// 依据12节气修正干支月
var gzM = this.toGanZhi((y - 1900) * 12 + m + 11)
if (d >= firstNode) {
gzM = this.toGanZhi((y - 1900) * 12 + m + 12)
}
// 传入的日期的节气与否
var isTerm = false
var Term = null
if (firstNode == d) {
isTerm = true
Term = this.solarTerm[m * 2 - 2]
}
if (secondNode == d) {
isTerm = true
Term = this.solarTerm[m * 2 - 1]
}
// 日柱 当月一日与 1900/1/1 相差天数
var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10
var gzD = this.toGanZhi(dayCyclical + d - 1)
// 该日期所属的星座
var astro = this.toAstro(m, d)
return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': this.getAnimal(year), 'IMonthCn': (isLeap ? '\u95f0' : '') + this.toChinaMonth(month), 'IDayCn': this.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': '\u661f\u671f' + cWeek, 'isTerm': isTerm, 'Term': Term, 'astro': astro }
},
/**
* 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON
* @param y lunar year
* @param m lunar month
* @param d lunar day
* @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
* @return JSON object
* @eg:console.log(calendar.lunar2solar(1987,9,10));
*/
lunar2solar: function (y, m, d, isLeapMonth) { // 参数区间1900.1.31~2100.12.1
var isLeapMonth = !!isLeapMonth
var leapOffset = 0
var leapMonth = this.leapMonth(y)
var leapDay = this.leapDays(y)
if (isLeapMonth && (leapMonth != m)) { return -1 }// 传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
if (y == 2100 && m == 12 && d > 1 || y == 1900 && m == 1 && d < 31) { return -1 }// 超出了最大极限值
var day = this.monthDays(y, m)
var _day = day
// bugFix 2016-9-25
// if month is leap, _day use leapDays method
if (isLeapMonth) {
_day = this.leapDays(y, m)
}
if (y < 1900 || y > 2100 || d > _day) { return -1 }// 参数合法性效验
// 计算农历的时间差
var offset = 0
for (var i = 1900; i < y; i++) {
offset += this.lYearDays(i)
}
var leap = 0; var isAdd = false
for (var i = 1; i < m; i++) {
leap = this.leapMonth(y)
if (!isAdd) { // 处理闰月
if (leap <= i && leap > 0) {
offset += this.leapDays(y); isAdd = true
}
}
offset += this.monthDays(y, i)
}
// 转换闰月农历 需补充该年闰月的前一个月的时差
if (isLeapMonth) { offset += day }
// 1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点)
var stmap = Date.UTC(1900, 1, 30, 0, 0, 0)
var calObj = new Date((offset + d - 31) * 86400000 + stmap)
var cY = calObj.getUTCFullYear()
var cM = calObj.getUTCMonth() + 1
var cD = calObj.getUTCDate()
return this.solar2lunar(cY, cM, cD)
}
}
export default calendar
@@ -0,0 +1,12 @@
{
"uni-calender.ok": "ok",
"uni-calender.cancel": "cancel",
"uni-calender.today": "today",
"uni-calender.MON": "MON",
"uni-calender.TUE": "TUE",
"uni-calender.WED": "WED",
"uni-calender.THU": "THU",
"uni-calender.FRI": "FRI",
"uni-calender.SAT": "SAT",
"uni-calender.SUN": "SUN"
}
@@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant
}
@@ -0,0 +1,12 @@
{
"uni-calender.ok": "确定",
"uni-calender.cancel": "取消",
"uni-calender.today": "今日",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}
@@ -0,0 +1,12 @@
{
"uni-calender.ok": "確定",
"uni-calender.cancel": "取消",
"uni-calender.today": "今日",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六"
}
@@ -0,0 +1,191 @@
<template>
<view class="uni-calendar-item__weeks-box"
:class="{
'uni-calendar-item--disable':weeks.disable,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':(calendar.fullDate === weeks.fullDate && !weeks.isDay) ,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
}"
@click="choiceDate(weeks)">
<view class="uni-calendar-item__weeks-box-item">
<text v-if="selected&&weeks.extraInfo" class="uni-calendar-item__weeks-box-circle">{{ weeks.price }}</text>
<text class="uni-calendar-item__weeks-box-text" :class="{
'uni-calendar-item--isDay-text': weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">{{weeks.date}}</text>
<text v-if="!lunar&&!weeks.extraInfo && weeks.isDay" class="uni-calendar-item__weeks-lunar-text" :class="{
'uni-calendar-item--isDay-text':weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
}">{{todayText}}</text>
<text v-if="lunar&&!weeks.extraInfo" class="uni-calendar-item__weeks-lunar-text" :class="{
'uni-calendar-item--isDay-text':weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">{{weeks.isDay ? todayText : (weeks.lunar.IDayCn === '初一'?weeks.lunar.IMonthCn:weeks.lunar.IDayCn)}}</text>
<text v-if="weeks.extraInfo&&weeks.extraInfo.info" class="uni-calendar-item__weeks-lunar-text" :class="{
'uni-calendar-item--extra':weeks.extraInfo.info,
'uni-calendar-item--isDay-text':weeks.isDay,
'uni-calendar-item--isDay':calendar.fullDate === weeks.fullDate && weeks.isDay,
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && !weeks.isDay,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">{{weeks.extraInfo.info}}</text>
</view>
</view>
</template>
<script>
import { initVueI18n } from '@dcloudio/uni-i18n'
import i18nMessages from './i18n/index.js'
const { t } = initVueI18n(i18nMessages)
export default {
emits:['change'],
props: {
weeks: {
type: Object,
default () {
return {}
}
},
calendar: {
type: Object,
default: () => {
return {}
}
},
selected: {
type: Array,
default: () => {
return []
}
},
lunar: {
type: Boolean,
default: false
}
},
computed: {
todayText() {
return t("uni-calender.today")
},
},
methods: {
choiceDate(weeks) {
this.$emit('change', weeks)
},
}
}
</script>
<style lang="scss" scoped>
$uni-font-size-base:14px;
$uni-text-color:#333;
$uni-font-size-sm:12px;
$uni-color-error: #e43d33;
$uni-opacity-disabled: 0.3;
$uni-text-color-disable:#c0c0c0;
$uni-primary: #00A88E !default;
.uni-calendar-item__weeks-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 100rpx;
}
.uni-calendar-item__weeks-box-text {
font-size: $uni-font-size-base;
color: $uni-text-color;
}
.uni-calendar-item__weeks-lunar-text {
font-size: $uni-font-size-sm;
color: $uni-text-color;
}
.uni-calendar-item__weeks-box-item {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
flex:1;
width: 80%;
}
.uni-calendar-item__weeks-box-circle {
position: absolute;
top: 5px;
right: 5px;
// width: 8px;
// height: 8px;
// border-radius: 8px;
// background-color: $uni-color-error;
}
.uni-calendar-item--disable {
background-color: rgba(249, 249, 249, $uni-opacity-disabled);
color: $uni-text-color-disable;
}
.uni-calendar-item--isDay-text {
color: $uni-primary;
}
.uni-calendar-item--isDay {
background-color: $uni-primary;
border-radius: 10px;
opacity: 0.8;
color: #fff;
}
.uni-calendar-item--extra {
color: $uni-color-error;
opacity: 0.8;
}
.uni-calendar-item--checked {
background-color: $uni-primary;
border-radius: 10px;
color: #fff;
opacity: 0.8;
}
.uni-calendar-item--multiple {
background-color: $uni-primary;
color: #fff;
opacity: 0.8;
}
.uni-calendar-item--before-checked {
background-color: #c4ebe5;
color: #fff;
}
.uni-calendar-item--after-checked {
background-color: #c4ebe5;
color: #fff;
}
</style>
@@ -0,0 +1,585 @@
<template>
<view class="uni-calendar">
<view v-if="!insert&&show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}"
@click="clean"></view>
<view v-if="insert || show" class="uni-calendar__content"
:class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow}">
<view v-if="!insert" class="uni-calendar__header uni-calendar--fixed-top">
<view class="uni-calendar__header-btn-box" @click="close">
<text class="uni-calendar__header-text uni-calendar--fixed-width">{{cancelText}}</text>
</view>
<view class="uni-calendar__header-btn-box" @click="confirm">
<text class="uni-calendar__header-text uni-calendar--fixed-width">{{okText}}</text>
</view>
</view>
<view class="uni-calendar__box">
<view v-if="showMonth" class="uni-calendar__box-bg">
<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
</view>
<view class="uni-calendar__header">
<view class="uni-calendar__header-btn-box" @click.stop="pre">
<view class="uni-calendar__header-btn uni-calendar--left"></view>
</view>
<picker mode="date" :value="date" fields="month" @change="bindDateChange">
<text
class="uni-calendar__header-text">{{ (nowDate.year||'') +' / '+( nowDate.month||'')}}</text>
</picker>
<view class="uni-calendar__header-btn-box" @click.stop="next">
<view class="uni-calendar__header-btn uni-calendar--right"></view>
</view>
<text class="uni-calendar__backtoday" @click="backToday">{{todayText}}</text>
</view>
<view class="uni-calendar__weeks border-b-1 border-solid border-theme">
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{monText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
</view>
</view>
<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar"
:selected="selected" :lunar="lunar" @change="choiceDate"></calendar-item>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import Calendar from './util.js';
import CalendarItem from './uni-calendar-item.vue'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import i18nMessages from './i18n/index.js'
const {
t
} = initVueI18n(i18nMessages)
/**
* Calendar 日历
* @description 日历组件可以查看日期,选择任意范围内的日期,打点操作。常用场景如:酒店日期预订、火车机票选择购买日期、上下班打卡等
* @tutorial https://ext.dcloud.net.cn/plugin?id=56
* @property {String} date 自定义当前时间,默认为今天
* @property {Boolean} lunar 显示农历
* @property {String} startDate 日期选择范围-开始日期
* @property {String} endDate 日期选择范围-结束日期
* @property {Boolean} range 范围选择
* @property {Boolean} insert = [true|false] 插入模式,默认为false
* @value true 弹窗模式
* @value false 插入模式
* @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
* @property {Array} selected 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
* @property {Boolean} showMonth 是否选择月份为背景
* @event {Function} change 日期改变,`insert :ture` 时生效
* @event {Function} confirm 确认选择`insert :false` 时生效
* @event {Function} monthSwitch 切换月份时触发
* @example <uni-calendar :insert="true":lunar="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
*/
export default {
components: {
CalendarItem
},
emits: ['close', 'confirm', 'change', 'monthSwitch'],
props: {
date: {
type: String,
default: ''
},
selected: {
type: Array,
default () {
return []
}
},
lunar: {
type: Boolean,
default: false
},
startDate: {
type: String,
default: ''
},
endDate: {
type: String,
default: ''
},
range: {
type: Boolean,
default: false
},
insert: {
type: Boolean,
default: true
},
showMonth: {
type: Boolean,
default: true
},
clearDate: {
type: Boolean,
default: true
}
},
data() {
return {
show: false,
weeks: [],
calendar: {},
nowDate: '',
aniMaskShow: false
}
},
computed: {
/**
* for i18n
*/
okText() {
return t("uni-calender.ok")
},
cancelText() {
return t("uni-calender.cancel")
},
todayText() {
return t("uni-calender.today")
},
monText() {
return t("uni-calender.MON")
},
TUEText() {
return t("uni-calender.TUE")
},
WEDText() {
return t("uni-calender.WED")
},
THUText() {
return t("uni-calender.THU")
},
FRIText() {
return t("uni-calender.FRI")
},
SATText() {
return t("uni-calender.SAT")
},
SUNText() {
return t("uni-calender.SUN")
},
},
watch: {
date(newVal) {
// this.cale.setDate(newVal)
this.init(newVal)
},
startDate(val) {
this.cale.resetSatrtDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
endDate(val) {
this.cale.resetEndDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
selected(newVal) {
this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
this.weeks = this.cale.weeks
}
},
created() {
this.cale = new Calendar({
selected: this.selected,
startDate: this.startDate,
endDate: this.endDate,
range: this.range,
})
this.init(this.date)
},
methods: {
// 取消穿透
clean() {},
bindDateChange(e) {
const value = e.detail.value + '-1'
this.setDate(value)
const {
year,
month
} = this.cale.getDate(value)
this.$emit('monthSwitch', {
year,
month
})
},
/**
* 初始化日期显示
* @param {Object} date
*/
init(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.calendar = this.cale.getInfo(date)
},
/**
* 打开日历弹窗
*/
open() {
// 弹窗模式并且清理数据
if (this.clearDate && !this.insert) {
this.cale.cleanMultipleStatus()
// this.cale.setDate(this.date)
this.init(this.date)
}
this.show = true
this.$nextTick(() => {
setTimeout(() => {
this.aniMaskShow = true
}, 50)
})
},
/**
* 关闭日历弹窗
*/
close() {
this.aniMaskShow = false
this.$nextTick(() => {
setTimeout(() => {
this.show = false
this.$emit('close')
}, 300)
})
},
/**
* 确认按钮
*/
confirm() {
this.setEmit('confirm')
this.close()
},
/**
* 变化触发
*/
change() {
if (!this.insert) return
this.setEmit('change')
},
/**
* 选择月份触发
*/
monthSwitch() {
let {
year,
month
} = this.nowDate
this.$emit('monthSwitch', {
year,
month: Number(month)
})
},
/**
* 派发事件
* @param {Object} name
*/
setEmit(name) {
let {
year,
month,
date,
fullDate,
lunar,
extraInfo
} = this.calendar
this.$emit(name, {
range: this.cale.multipleStatus,
year,
month,
date,
fulldate: fullDate,
lunar,
extraInfo: extraInfo || {}
})
},
/**
* 选择天触发
* @param {Object} weeks
*/
choiceDate(weeks) {
if (weeks.disable) return
this.calendar = weeks
// 设置多选
this.cale.setMultiple(this.calendar.fullDate)
this.weeks = this.cale.weeks
this.change()
},
/**
* 回到今天
*/
backToday() {
const nowYearMonth = `${this.nowDate.year}-${this.nowDate.month}`
const date = this.cale.getDate(new Date())
const todayYearMonth = `${date.year}-${date.month}`
this.init(date.fullDate)
if (nowYearMonth !== todayYearMonth) {
this.monthSwitch()
}
this.change()
},
/**
* 上个月
*/
pre() {
const preDate = this.cale.getDate(this.nowDate.fullDate, -1, 'month').fullDate
this.setDate(preDate)
this.monthSwitch()
},
/**
* 下个月
*/
next() {
const nextDate = this.cale.getDate(this.nowDate.fullDate, +1, 'month').fullDate
this.setDate(nextDate)
this.monthSwitch()
},
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.cale.getInfo(date)
}
}
}
</script>
<style lang="scss" scoped>
$uni-bg-color-mask: rgba($color: #000000, $alpha: 0.4);
$uni-border-color: #00A88E;
$uni-text-color: #333;
$uni-bg-color-hover: #f1f1f1;
$uni-font-size-base: 14px;
$uni-text-color-placeholder: #808080;
$uni-color-subtitle: #555555;
$uni-text-color-grey: #999;
.uni-calendar {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
padding: 0 20px;
}
.uni-calendar__mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: $uni-bg-color-mask;
transition-property: opacity;
transition-duration: 0.3s;
opacity: 0;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--mask-show {
opacity: 1
}
.uni-calendar--fixed {
position: fixed;
/* #ifdef APP-NVUE */
bottom: 0;
/* #endif */
left: 0;
right: 0;
transition-property: transform;
transition-duration: 0.3s;
transform: translateY(460px);
/* #ifndef APP-NVUE */
bottom: calc(var(--window-bottom));
z-index: 99;
/* #endif */
}
.uni-calendar--ani-show {
transform: translateY(0);
}
.uni-calendar__content {
background-color: #fff;
}
.uni-calendar__header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
height: 50px;
border-bottom-color: $uni-border-color;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.uni-calendar--fixed-top {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
border-top-color: $uni-border-color;
border-top-style: solid;
border-top-width: 1px;
}
.uni-calendar--fixed-width {
width: 50px;
}
.uni-calendar__backtoday {
position: absolute;
right: 0;
top: 25rpx;
padding: 0 5px;
padding-left: 10px;
height: 25px;
line-height: 25px;
font-size: 12px;
border-top-left-radius: 25px;
border-bottom-left-radius: 25px;
color: $uni-text-color;
background-color: $uni-bg-color-hover;
}
.uni-calendar__header-text {
text-align: center;
width: 100px;
font-size: $uni-font-size-base;
color: $uni-text-color;
}
.uni-calendar__header-btn-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
}
.uni-calendar__header-btn {
width: 10px;
height: 10px;
border-left-color: $uni-text-color-placeholder;
border-left-style: solid;
border-left-width: 2px;
border-top-color: $uni-color-subtitle;
border-top-style: solid;
border-top-width: 2px;
}
.uni-calendar--left {
transform: rotate(-45deg);
}
.uni-calendar--right {
transform: rotate(135deg);
}
.uni-calendar__weeks {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-calendar__weeks-item {
flex: 1;
}
.uni-calendar__weeks-day {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 45px;
border-bottom-color: #00A88E;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.uni-calendar__weeks-day-text {
font-size: 14px;
}
.uni-calendar__box {
position: relative;
border: 2rpx solid #00A88E;
border-radius: 10rpx;
padding: 10px;
background:rgb(0 255 220 / 5%);
}
.uni-calendar__box-bg {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.uni-calendar__box-bg-text {
font-size: 200px;
font-weight: bold;
color: $uni-text-color-grey;
opacity: 0.1;
text-align: center;
/* #ifndef APP-NVUE */
line-height: 1;
/* #endif */
}
</style>
@@ -0,0 +1,360 @@
import CALENDAR from './calendar.js'
class Calendar {
constructor({
date,
selected,
startDate,
endDate,
range
} = {}) {
// 当前日期
this.date = this.getDate(new Date()) // 当前初入日期
// 打点信息
this.selected = selected || [];
// 范围开始
this.startDate = startDate
// 范围结束
this.endDate = endDate
this.range = range
// 多选状态
this.cleanMultipleStatus()
// 每周日期
this.weeks = {}
// this._getWeek(this.date.fullDate)
}
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.selectDate = this.getDate(date)
this._getWeek(this.selectDate.fullDate)
}
/**
* 清理多选状态
*/
cleanMultipleStatus() {
this.multipleStatus = {
before: '',
after: '',
data: []
}
}
/**
* 重置开始日期
*/
resetSatrtDate(startDate) {
// 范围开始
this.startDate = startDate
}
/**
* 重置结束日期
*/
resetEndDate(endDate) {
// 范围结束
this.endDate = endDate
}
/**
* 获取任意时间
*/
getDate(date, AddDayCount = 0, str = 'day') {
if (!date) {
date = new Date()
}
if (typeof date !== 'object') {
date = date.replace(/-/g, '/')
}
const dd = new Date(date)
switch (str) {
case 'day':
dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期
break
case 'month':
if (dd.getDate() === 31 && AddDayCount>0) {
dd.setDate(dd.getDate() + AddDayCount)
} else {
const preMonth = dd.getMonth()
dd.setMonth(preMonth + AddDayCount) // 获取AddDayCount天后的日期
const nextMonth = dd.getMonth()
// 处理 pre 切换月份目标月份为2月没有当前日(30 31) 切换错误问题
if(AddDayCount<0 && preMonth!==0 && nextMonth-preMonth>AddDayCount){
dd.setMonth(nextMonth+(nextMonth-preMonth+AddDayCount))
}
// 处理 next 切换月份目标月份为2月没有当前日(30 31) 切换错误问题
if(AddDayCount>0 && nextMonth-preMonth>AddDayCount){
dd.setMonth(nextMonth-(nextMonth-preMonth-AddDayCount))
}
}
break
case 'year':
dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期
break
}
const y = dd.getFullYear()
const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0
const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0
return {
fullDate: y + '-' + m + '-' + d,
year: y,
month: m,
date: d,
day: dd.getDay()
}
}
/**
* 获取上月剩余天数
*/
_getLastMonthDays(firstDay, full) {
let dateArr = []
for (let i = firstDay; i > 0; i--) {
const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate()
dateArr.push({
date: beforeDate,
month: full.month - 1,
lunar: this.getlunar(full.year, full.month - 1, beforeDate),
disable: true
})
}
return dateArr
}
/**
* 获取本月天数
*/
_currentMonthDys(dateData, full) {
let dateArr = []
let fullDate = this.date.fullDate
for (let i = 1; i <= dateData; i++) {
let nowDate = full.year + '-' + (full.month < 10 ?
full.month : full.month) + '-' + (i < 10 ?
'0' + i : i)
// 是否今天
let isDay = fullDate === nowDate
// 获取打点信息
let info = this.selected && this.selected.find((item) => {
if (this.dateEqual(nowDate, item.date)) {
return item
}
})
// 日期禁用
let disableBefore = true
let disableAfter = true
if (this.startDate) {
// let dateCompBefore = this.dateCompare(this.startDate, fullDate)
// disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate)
disableBefore = this.dateCompare(this.startDate, nowDate)
}
if (this.endDate) {
// let dateCompAfter = this.dateCompare(fullDate, this.endDate)
// disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate)
disableAfter = this.dateCompare(nowDate, this.endDate)
}
let multiples = this.multipleStatus.data
let checked = false
let multiplesStatus = -1
if (this.range) {
if (multiples) {
multiplesStatus = multiples.findIndex((item) => {
return this.dateEqual(item, nowDate)
})
}
if (multiplesStatus !== -1) {
checked = true
}
}
let data = {
fullDate: nowDate,
year: full.year,
date: i,
multiple: this.range ? checked : false,
beforeMultiple: this.dateEqual(this.multipleStatus.before, nowDate),
afterMultiple: this.dateEqual(this.multipleStatus.after, nowDate),
month: full.month,
lunar: this.getlunar(full.year, full.month, i),
disable: !(disableBefore && disableAfter),
isDay
}
if (info) {
data.extraInfo = info
}
dateArr.push(data)
}
return dateArr
}
/**
* 获取下月天数
*/
_getNextMonthDays(surplus, full) {
let dateArr = []
for (let i = 1; i < surplus + 1; i++) {
dateArr.push({
date: i,
month: Number(full.month) + 1,
lunar: this.getlunar(full.year, Number(full.month) + 1, i),
disable: true
})
}
return dateArr
}
/**
* 获取当前日期详情
* @param {Object} date
*/
getInfo(date) {
if (!date) {
date = new Date()
}
const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate)
return dateInfo
}
/**
* 比较时间大小
*/
dateCompare(startDate, endDate) {
// 计算截止时间
startDate = new Date(startDate.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
endDate = new Date(endDate.replace('-', '/').replace('-', '/'))
if (startDate <= endDate) {
return true
} else {
return false
}
}
/**
* 比较时间是否相等
*/
dateEqual(before, after) {
// 计算截止时间
before = new Date(before.replace('-', '/').replace('-', '/'))
// 计算详细项的截止时间
after = new Date(after.replace('-', '/').replace('-', '/'))
if (before.getTime() - after.getTime() === 0) {
return true
} else {
return false
}
}
/**
* 获取日期范围内所有日期
* @param {Object} begin
* @param {Object} end
*/
geDateAll(begin, end) {
var arr = []
var ab = begin.split('-')
var ae = end.split('-')
var db = new Date()
db.setFullYear(ab[0], ab[1] - 1, ab[2])
var de = new Date()
de.setFullYear(ae[0], ae[1] - 1, ae[2])
var unixDb = db.getTime() - 24 * 60 * 60 * 1000
var unixDe = de.getTime() - 24 * 60 * 60 * 1000
for (var k = unixDb; k <= unixDe;) {
k = k + 24 * 60 * 60 * 1000
arr.push(this.getDate(new Date(parseInt(k))).fullDate)
}
return arr
}
/**
* 计算阴历日期显示
*/
getlunar(year, month, date) {
return CALENDAR.solar2lunar(year, month, date)
}
/**
* 设置打点
*/
setSelectInfo(data, value) {
this.selected = value
this._getWeek(data)
}
/**
* 获取多选状态
*/
setMultiple(fullDate) {
let {
before,
after
} = this.multipleStatus
if (!this.range) return
if (before && after) {
this.multipleStatus.before = fullDate
this.multipleStatus.after = ''
this.multipleStatus.data = []
} else {
if (!before) {
this.multipleStatus.before = fullDate
} else {
this.multipleStatus.after = fullDate
if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after);
} else {
this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before);
}
}
}
this._getWeek(fullDate)
}
/**
* 获取每周数据
* @param {Object} dateData
*/
_getWeek(dateData) {
const {
year,
month
} = this.getDate(dateData)
let firstDay = new Date(year, month - 1, 1).getDay()
let currentDay = new Date(year, month, 0).getDate()
let dates = {
lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天
currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数
nextMonthDays: [], // 下个月开始几天
weeks: []
}
let canlender = []
const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length)
dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData))
canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays)
let weeks = {}
// 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天
for (let i = 0; i < canlender.length; i++) {
if (i % 7 === 0) {
weeks[parseInt(i / 7)] = new Array(7)
}
weeks[parseInt(i / 7)][i % 7] = canlender[i]
}
this.canlender = canlender
this.weeks = weeks
}
//静态方法
// static init(date) {
// if (!this.instance) {
// this.instance = new Calendar(date);
// }
// return this.instance;
// }
}
export default Calendar
+86
View File
@@ -0,0 +1,86 @@
{
"id": "uni-calendar",
"displayName": "uni-calendar 日历",
"version": "1.4.12",
"description": "日历组件",
"keywords": [
"uni-ui",
"uniui",
"日历",
"",
"打卡",
"日历选择"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}
+103
View File
@@ -0,0 +1,103 @@
## Calendar 日历
> **组件名:uni-calendar**
> 代码块: `uCalendar`
日历组件
> **注意事项**
> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。
> - 本组件农历转换使用的js是 [@1900-2100区间内的公历、农历互转](https://github.com/jjonline/calendar.js)
> - 仅支持自定义组件模式
> - `date`属性传入的应该是一个 String ,如: 2019-06-27 ,而不是 new Date()
> - 通过 `insert` 属性来确定当前的事件是 @change 还是 @confirm 。理应合并为一个事件,但是为了区分模式,现使用两个事件,这里需要注意
> - 弹窗模式下无法阻止后面的元素滚动,如有需要阻止,请在弹窗弹出后,手动设置滚动元素为不可滚动
### 安装方式
本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。
如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55)
### 基本用法
在 ``template`` 中使用组件
```html
<view>
<uni-calendar
:insert="true"
:lunar="true"
:start-date="'2019-3-2'"
:end-date="'2019-5-20'"
@change="change"
/>
</view>
```
### 通过方法打开日历
需要设置 `insert` 为 `false`
```html
<view>
<uni-calendar
ref="calendar"
:insert="false"
@confirm="confirm"
/>
<button @click="open">打开日历</button>
</view>
```
```javascript
export default {
data() {
return {};
},
methods: {
open(){
this.$refs.calendar.open();
},
confirm(e) {
console.log(e);
}
}
};
```
## API
### Calendar Props
| 属性名 | 类型 | 默认值| 说明 |
| - | - | - | - |
| date | String |- | 自定义当前时间,默认为今天 |
| lunar | Boolean | false | 显示农历 |
| startDate | String |- | 日期选择范围-开始日期 |
| endDate | String |- | 日期选择范围-结束日期 |
| range | Boolean | false | 范围选择 |
| insert | Boolean | false | 插入模式,可选值,ture:插入模式;false:弹窗模式;默认为插入模式 |
|clearDate |Boolean |true |弹窗模式是否清空上次选择内容 |
| selected | Array |- | 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}] |
|showMonth | Boolean | true | 是否显示月份为背景 |
### Calendar Events
| 事件名 | 说明 |返回值|
| - | - | - |
| open | 弹出日历组件,`insert :false` 时生效|- |
## 组件示例
点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/calendar/calendar](https://hellouniapp.dcloud.net.cn/pages/extUI/calendar/calendar)
+135
View File
@@ -0,0 +1,135 @@
import { useUserStore } from '@/store/user';
export const baseUrl = import.meta.env.VITE_APP_BASE_URL;
const appVersion = uni.getSystemInfoSync().appVersion;
// 添加拦截器
// const httpInterceptor = {
// // 拦截前触发
// invoke(options) {
// // 1. 非 http 开头需拼接地址
// if (!options.url.startsWith('http')) {
// options.url = baseUrl + options.url;
// }
// // 2. 请求超时, 默认 10s
// options.timeout = 10000;
// options.header = {
// ...options.header,
// 'Content-Type': 'application/x-www-form-urlencoded',
// };
// const userStore = useUserStore();
// options.data = {
// ...options.data,
// __token: userStore.userToken,
// __platform: userStore.platform,
// };
// },
// };
// uni.addInterceptor('request', httpInterceptor);
// // #ifndef H5
// uni.addInterceptor('uploadFile', httpInterceptor);
// // #endif
// 对uni原生的request做的一个封装
export const request = (options = {}) => {
return new Promise((resolve, reject) => {
const userStore = useUserStore();
// 请求头
options.header = {
'Content-Type': 'application/x-www-form-urlencoded',
// version: appVersion,
};
var params = {
__token: userStore.userToken,
__platform: userStore.platform,
__language: userStore.lang,
__channelId: 33,
};
var data = options.data;
data = {
...data,
...params,
};
// uni原生request
uni
.request({
url: baseUrl + options.url || '',
method: options.method || 'GET',
data: data || {},
header: options.header || {},
})
.then((res) => {
if (res.statusCode === 401) {
// usetToken无效,移除本地缓存
userStore.userToken = '';
userStore.clearProfile();
userStore.isLogin = false;
resolve({
...res.data,
statusCode: res.statusCode,
});
} else {
resolve({
...res.data,
statusCode: res.statusCode,
});
}
})
.catch((error) => {
if (error.statusCode === 401) {
// usetToken无效,移除本地缓存
userStore.userToken = '';
userStore.clearProfile();
reject({
...error.data,
statusCode: error.statusCode,
});
} else {
uni.showToast({
duration: 2500,
icon: 'none',
mask: true,
title: 'Server exception, please try again later',
});
setTimeout(() => {
uni.hideLoading();
reject(error.data);
}, 2000);
}
});
});
};
// 定义get请求
export const get = (url, data = {}, options = {}) => {
options.url = url;
options.method = 'GET';
options.data = data;
return request(options);
};
// 定义post请求
export const post = (url, data = {}, options = {}) => {
options.url = url;
options.method = 'POST';
options.data = data;
return request(options);
};
// 定义put请求
export const put = (url, data = {}, options = {}) => {
options.url = url;
options.method = 'PUT';
options.data = data;
return request(options);
};
// 定义delete请求
export const dele = (url, data = {}, options = {}) => {
options.url = url;
options.method = 'DELETE';
options.data = data;
return request(options);
};
+64
View File
@@ -0,0 +1,64 @@
import { defineConfig, loadEnv } from 'vite';
import path from 'path';
import uni from '@dcloudio/vite-plugin-uni';
// start 引入tailwindcss增加的配置0
import { UnifiedViteWeappTailwindcssPlugin as uvwt } from 'weapp-tailwindcss/vite';
// 注意: 打包成 h5 和 app 都不需要开启插件配置
const isH5 = process.env.UNI_PLATFORM === 'h5';
const isApp = process.env.UNI_PLATFORM === 'app';
const WeappTailwindcssDisabled = isH5 || isApp;
const resolve = (p) => {
return path.resolve(__dirname, p);
};
export default defineConfig(({ command, mode }) => {
// 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
const env = loadEnv(mode, process.cwd(), '');
return {
// vite 配置
base: './',
define: {},
resolve: {
alias: {
'@': '/pages',
},
},
server: {
proxy: {
'/sale': {
target: process.env.VITE_APP_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/sale/, '/'),
},
},
},
plugins: [
uni(),
// 引入tailwindcss增加的配置plugins
uvwt({
rem2rpx: true,
disabled: WeappTailwindcssDisabled,
// 由于 hbuilderx 会改变 process.cwd 所以这里必须传入当前目录的绝对路径
tailwindcssBasedir: __dirname,
}),
],
// 引入tailwindcss增加的配置postcss
css: {
postcss: {
plugins: [
require('tailwindcss')({
// 注意此处,手动传入你 `tailwind.config.js` 的绝对路径
config: resolve('./tailwind.config.js'),
}),
require('autoprefixer'),
],
},
},
ssr: {
format: 'cjs', //esm,cjs即将在vite5移除
},
};
});