feat:微信小程序一键登录页面
No files matched your search
@@ -1,13 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
|
||||
onLaunch(() => {
|
||||
console.log("App Launch");
|
||||
});
|
||||
onShow(() => {
|
||||
console.log("App Show");
|
||||
});
|
||||
onHide(() => {
|
||||
console.log("App Hide");
|
||||
});
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
onLaunch() {
|
||||
console.log("App Launch");
|
||||
},
|
||||
onShow() {
|
||||
console.log("App Show");
|
||||
},
|
||||
onHide() {
|
||||
console.log("App Hide");
|
||||
},
|
||||
async _initTabbar() {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
<style lang="scss">
|
||||
// tm-ui
|
||||
@import url(@/uni_modules/tm-ui/css/remixicon.min.css);
|
||||
@import url(@/uni_modules/tm-ui/css/tmui.css);
|
||||
// 全局
|
||||
@import "@/sheep/scss/global.scss";
|
||||
|
||||
$theme-green: #06be36;
|
||||
$theme-blue: #0094ff;
|
||||
$theme-gray: #999;
|
||||
|
||||
.botder_btn {
|
||||
border: 1px solid #999;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
border-radius: 10rpx;
|
||||
padding: 8rpx 15rpx;
|
||||
}
|
||||
|
||||
.prefix_title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 8rpx;
|
||||
height: 24rpx;
|
||||
background: $theme-blue;
|
||||
border-radius: 4rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
declare const __TABBAR__: Array<string>
|
||||
declare const __ROUTES__: Array<any>
|
||||
|
||||
declare namespace GlobalSetting {
|
||||
// 记住密码
|
||||
type RememberUser = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { createSSRApp } from "vue"
|
||||
import "uno.css"
|
||||
|
||||
import App from "./App.vue"
|
||||
import tmUi from "./uni_modules/tm-ui"
|
||||
|
||||
import pinia from "@@/stores/index"
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
return {
|
||||
app,
|
||||
};
|
||||
const app = createSSRApp(App)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(tmUi)
|
||||
|
||||
return {
|
||||
app,
|
||||
pinia,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,71 @@
|
||||
{
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^base-(.*)": "@/sheep/components/base/base-$1/base-$1.vue",
|
||||
"^crud-(.*)": "@/sheep/components/crud/crud-$1/crud-$1.vue",
|
||||
"^tm-(.*)": "@/uni_modules/tm-ui/components/tm-$1/tm-$1.vue",
|
||||
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"path": "pages/tabbar/canteen",
|
||||
"style": {
|
||||
"navigationBarTitleText": "uni-app"
|
||||
"navigationBarTitleText": "食堂",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/root/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tabbar/order",
|
||||
"style": {
|
||||
"navigationBarTitleText": "订单",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tabbar/my",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"disableScroll": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
"navigationBarTitleText": "浦江县机关事务智慧后勤一体化平台app",
|
||||
"navigationBarBackgroundColor": "#1777FF",
|
||||
"backgroundColor": "#ecf4f9",
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"tabBar": {
|
||||
"selectedColor": "#1777FF",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/tabbar/canteen",
|
||||
"text": "食堂",
|
||||
"iconPath": "/static/tabbar/canteen.png",
|
||||
"selectedIconPath": "/static/tabbar/canteen-select.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/tabbar/order",
|
||||
"text": "订单",
|
||||
"iconPath": "/static/tabbar/order.png",
|
||||
"selectedIconPath": "/static/tabbar/order-select.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/tabbar/my",
|
||||
"text": "我的",
|
||||
"iconPath": "/static/tabbar/my.png",
|
||||
"selectedIconPath": "/static/tabbar/my-select.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<image class="logo" src="/static/logo.png" />
|
||||
<view class="text-area">
|
||||
<text class="title">{{ title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const title = ref('Hello')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 200rpx;
|
||||
width: 200rpx;
|
||||
margin-top: 200rpx;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
color: #8f8f94;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue"
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app"
|
||||
import { constant, router } from "@@/index"
|
||||
import { userApi } from "@@/api"
|
||||
|
||||
import AffirmCheck from "@/sheep/components/common/affirm-check.vue"
|
||||
import AffirmModal from "@/sheep/components/common/affirm-modal.vue"
|
||||
|
||||
const affirmModalRef = ref()
|
||||
const state = reactive<any>({
|
||||
userInfo: null,
|
||||
isAgree: false,
|
||||
})
|
||||
const applyForm = reactive<any>({
|
||||
username: "",
|
||||
password: "",
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
const rememberUser = uni.getStorageSync(constant.storageConstant.REMEMBER_USER)
|
||||
if (rememberUser) {
|
||||
applyForm.username = rememberUser.username
|
||||
applyForm.password = rememberUser.password
|
||||
}
|
||||
})
|
||||
onShow(() => {
|
||||
const userInfoKey = constant.storageConstant.USERINFO
|
||||
state.userInfo = uni.getStorageSync(userInfoKey)
|
||||
})
|
||||
|
||||
|
||||
const applyFormRef = ref<any>(null)
|
||||
const doSubmit = async () => {
|
||||
applyFormRef.value.submit()
|
||||
const { isPass } = applyFormRef.value.validate()
|
||||
if (!isPass) return
|
||||
const tokenKey = constant.storageConstant.TOKEN
|
||||
const userInfoKey = constant.storageConstant.USERINFO
|
||||
const rememberUser = constant.storageConstant.REMEMBER_USER
|
||||
uni.removeStorageSync(tokenKey)
|
||||
uni.showLoading({ title: "登录中...", mask: true })
|
||||
const res: any = await userApi.login({
|
||||
...applyForm,
|
||||
grant_type: "password",
|
||||
scope: "server",
|
||||
})
|
||||
if (res.code != 0) {
|
||||
uni.showToast({ title: res.msg + ",登录失败,请重新登录", icon: "error" })
|
||||
return
|
||||
}
|
||||
uni.setStorageSync(tokenKey, res.data)
|
||||
const { data: userData } = await userApi.userInfo()
|
||||
uni.setStorageSync(userInfoKey, userData)
|
||||
uni.setStorageSync(rememberUser, {
|
||||
username: applyForm.username,
|
||||
password: applyForm.password,
|
||||
} as GlobalSetting.RememberUser)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: "登录成功", icon: "none" })
|
||||
router.jump("/pages/tabbar/canteen")
|
||||
}
|
||||
|
||||
// 小程序获取手机号
|
||||
const minAppGetphone = (e) => {
|
||||
const phoneDetail = e?.detail
|
||||
if (!phoneDetail) {
|
||||
uni.showToast({ title: "获取手机号失败", icon: "none" })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 没有授权点击登录
|
||||
const doLogin = () => {
|
||||
// 没有同意
|
||||
if (!state.isAgree) {
|
||||
affirmModalRef.value.show()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout title="登录" bg="/static/page/blue-bg.png">
|
||||
<view class="h-30rpx" />
|
||||
|
||||
<view class="flex-col-center m-t-160rpx">
|
||||
<view class="size-220rpx bg-white rounded-full flex-row-center">
|
||||
<image class="size-200rpx rounded-full" src="/static/logo/jgsw.jpeg" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="m-t-20rpx sub-text-28">浦江县机关事务智慧后勤一体化平台</view>
|
||||
</view>
|
||||
|
||||
<view class="m-t-160rpx">
|
||||
<button v-if="!state.isAgree" class="bg-#0094ff color-white" @click="doLogin">
|
||||
手机号一键登录
|
||||
</button>
|
||||
<button v-else class="bg-#0094ff color-white" open-type="getPhoneNumber" @getphonenumber="minAppGetphone">
|
||||
手机号一键登录
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<affirm-check v-model="state.isAgree" class="absolute bottom-30rpx" />
|
||||
<affirm-modal ref="affirmModalRef" @confirm="state.isAgree = true" />
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep() {
|
||||
.tmFormItemLabel {
|
||||
padding-bottom: 10rpx !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick } from "vue"
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
import { hooks, router, constant } from "@@/index"
|
||||
import { accessStatsApi, merchantApi } from "@@/api"
|
||||
import { filePath } from "@@/utils/file"
|
||||
|
||||
const state = reactive<any>({
|
||||
pa: {
|
||||
pagesize: 10,
|
||||
type: "1",
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
const app = getApp({ allowDefault: true })
|
||||
console.log("app:", app);
|
||||
|
||||
app._initTabbar()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const app = getApp({ allowDefault: true })
|
||||
app._initTabbar()
|
||||
reportPageAccess("pages/tabbar/canteen")
|
||||
if (pageRef.value) {
|
||||
pageRef.value.reload(false)
|
||||
}
|
||||
})
|
||||
|
||||
const pageRef = ref<any>(null)
|
||||
const pageHook = hooks.usePage(pageRef, merchantApi.page, null, "data.records")
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: constant.storageConstant.TOKEN,
|
||||
USER_INFO: constant.storageConstant.USERINFO,
|
||||
}
|
||||
const query = (page: number, pagesize: number) => {
|
||||
pageHook.startPage(page, pagesize, false, state.pa)
|
||||
}
|
||||
|
||||
const toDetail = async (v: any) => {
|
||||
const isLogin = await hooks.useCheckLogin()
|
||||
if (isLogin) {
|
||||
router.jump("/pages/canteen/detail", { id: v.id })
|
||||
}
|
||||
}
|
||||
|
||||
const createEventId = () => `evt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
|
||||
const reportPageAccess = async (pageCode: string) => {
|
||||
const tokenStorage: any = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
const userInfo: any = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
if (!userInfo?.sysUser?.userId) return
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
await accessStatsApi.track({
|
||||
eventId: createEventId(),
|
||||
pageCode,
|
||||
userId: userInfo?.sysUser?.userId,
|
||||
eventTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
||||
deviceId: tokenStorage?.access_token || "",
|
||||
clientType: systemInfo?.platform || "",
|
||||
osVersion: systemInfo?.system || "",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout title="食堂" bg="/static/page/blue-bg.png">
|
||||
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
|
||||
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query"
|
||||
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
|
||||
<view class="h-20rpx" />
|
||||
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
|
||||
<image class="w-180rpx h-180rpx rounded-12rpx" :src="filePath(v.logoPic)" mode="aspectFill" />
|
||||
<view class="flex-1 flex-col m-l-20rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
<view class="flex-row-center sub-text-34">{{ v.name }}</view>
|
||||
<view>
|
||||
<tm-tag size="s">{{ v.deptName }}</tm-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="second-text-24">
|
||||
<view class="second-text-28">营业时间:</view>
|
||||
<view>{{ v.jobTime?.split(",")[0] ? "早:" + v.jobTime?.split(",")[0] : "" }}</view>
|
||||
<view>{{ v.jobTime?.split(",")[1] ? "中:" + v.jobTime?.split(",")[1] : "" }}</view>
|
||||
<view>{{ v.jobTime?.split(",")[2] ? "晚:" + v.jobTime?.split(",")[2] : "" }}</view>
|
||||
</view>
|
||||
<view v-if="v.addressDes" class="m-t-10rpx second-text-28">
|
||||
<text>食堂地址</text>
|
||||
<text class="m-l-10rpx">{{ v.addressDes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
|
||||
</template>
|
||||
</z-paging>
|
||||
</crud-layout>
|
||||
</template>
|
||||
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<crud-layout title="我的" mainIsScroll bg="/static/page/blue-bg.png" navRightIcon="setting"
|
||||
@navRightClick="onNavRightClick">
|
||||
<template #main-top>
|
||||
<view class="flex items-center py-3 mx-30rpx">
|
||||
<image v-if="state.userInfo?.sysUser?.avatar && false" class="size-100rpx rounded-full"
|
||||
:src="filePath(state.userInfo.sysUser.avatar)" mode="aspectFill" />
|
||||
<image v-else class="w-100rpx h-100rpx rounded-full" src="/static/icon/default-avatar.png"
|
||||
mode="aspectFill" />
|
||||
<view class="ml-30rpx" v-if="state.userInfo?.sysUser?.userId">
|
||||
<view class="sub-text-30">{{ state.userInfo?.sysUser?.name }}</view>
|
||||
<view class="sub-text-26">ID {{ state.userInfo?.sysUser?.userId }}</view>
|
||||
</view>
|
||||
<view v-else class="flex items-center ml-30rpx" @tap="onTabLogin">
|
||||
<text class="sub-text-32">请先登录</text>
|
||||
<text class="iconfont icon-youjiantou" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="mt-10rpx rounded-20rpx shadow-main p-30rpx pb-40rpx bg-white">
|
||||
<view class="service_item flex items-center justify-between" v-for="(v, i) in state.menuList" :key="i"
|
||||
@tap="onTabMenu(v)">
|
||||
<view class="flex-row-center">
|
||||
<image class="size-40rpx" :src="v.icon" mode="aspectFill" />
|
||||
<text class="sub-text-28 m-l-20rpx">{{ v.text }}</text>
|
||||
</view>
|
||||
<view class="iconfont icon-youjiantou" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-col-center mt-40rpx mx-30rpx">
|
||||
<view class="w-full text-center sub-text-28 p-y-20rpx rounded-40rpx border-solid border-#0094ff"
|
||||
@tap="doLogout">
|
||||
退出登录
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-30rpx" />
|
||||
<view class="flex-col-center mt-40rpx mx-30rpx">
|
||||
<view class="w-full text-center sub-text-28 p-y-20rpx rounded-40rpx border-solid border-#0094ff"
|
||||
@tap="doChangeRole">
|
||||
切换角色
|
||||
</view>
|
||||
</view>
|
||||
</crud-layout>
|
||||
|
||||
<tm-modal :show="state.actModalShow" :showCancel="false" title="核销活动兑换券" zIndex="999" @confirm="" @cancel=""
|
||||
@close="">
|
||||
<view class="text-center text-28">是否核销该活动兑换券?</view>
|
||||
<view class="text-center text-28">确认核销将自动对用户的 消费钱包 扣费</view>
|
||||
<template #footer>
|
||||
<view class="w-full flex justify-center gap-6">
|
||||
<tm-button skin="thin" @tap="actModalCancel">取 消</tm-button>
|
||||
<tm-button color="primary" @tap="actModalConfirm">确 认</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from "vue"
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app"
|
||||
import { isEmpty } from "radash"
|
||||
import dayjs from "dayjs"
|
||||
import { filePath } from "@@/utils/file"
|
||||
import { getAppInfo } from "@@/utils/app"
|
||||
import { hooks, router, constant } from "@@/index"
|
||||
import { accessStatsApi, actCancelApi, userApi } from "@@/api"
|
||||
import { APPENV } from "@/sheep/constant"
|
||||
|
||||
const state = reactive<any>({
|
||||
wallet: [],
|
||||
userInfo: {},
|
||||
menuList: [
|
||||
// { text: "扫一扫", icon: "/static/icon/scan.png", path: "" },
|
||||
{ text: "钱包", icon: "/static/icon/service_3.png", path: "/pages/wallet/list" },
|
||||
{ text: "喜好", icon: "/static/icon/service_4.png", path: "/pages/tabbar/canteenAnalyse" },
|
||||
// { text: "我的人脸", icon: "/static/icon/service_5.png", path: "/pages/human-face/list" },
|
||||
{ text: "赚米", icon: "/static/icon/service_7.png", path: "/pages/ad/make-money" },
|
||||
// { text: "车辆管理", icon: "/static/icon/service_6.png", path: "/pages/car/user-car/list" },
|
||||
],
|
||||
appInfo: "",
|
||||
actModalShow: false,
|
||||
actVal: {},
|
||||
})
|
||||
|
||||
// 常量
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: constant.storageConstant.TOKEN,
|
||||
USER_INFO: constant.storageConstant.USERINFO,
|
||||
}
|
||||
|
||||
const storage = {
|
||||
// 获取存储的用户信息
|
||||
getUserInfo: () => {
|
||||
const tokenStorage = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
const userStorage = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
|
||||
if (!isEmpty(userStorage) && !isEmpty(tokenStorage)) {
|
||||
state.userInfo = userStorage
|
||||
} else {
|
||||
state.userInfo = {}
|
||||
}
|
||||
},
|
||||
|
||||
// 保存用户信息到存储
|
||||
saveUserInfo: (userData: any) => {
|
||||
uni.setStorageSync(STORAGE_KEYS.USER_INFO, userData)
|
||||
storage.getUserInfo()
|
||||
},
|
||||
|
||||
// 保存token到存储
|
||||
saveToken: (tokenData: any) => {
|
||||
uni.setStorageSync(STORAGE_KEYS.TOKEN, tokenData)
|
||||
},
|
||||
|
||||
// 清除用户登录相关的存储
|
||||
clearUserStorage: () => {
|
||||
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
state.userInfo = {}
|
||||
},
|
||||
}
|
||||
|
||||
const login = {
|
||||
// 检查应用环境并登录
|
||||
checkAppInfo: async () => {
|
||||
try {
|
||||
// 先尝试诗画登录
|
||||
const shihuaResult = await login.shihuaLogin()
|
||||
if (shihuaResult) return
|
||||
|
||||
// 尝试浙政钉登录
|
||||
const zhengdingResult = login.zhengdingLogin()
|
||||
if (zhengdingResult) return
|
||||
|
||||
uni.showToast({ title: "当前登录环境未知", icon: "none" })
|
||||
} catch (error) {
|
||||
console.error("登录失败:", error)
|
||||
uni.showToast({ title: "登录失败", icon: "none" })
|
||||
}
|
||||
},
|
||||
|
||||
// 诗画环境登录
|
||||
shihuaLogin: async () => {
|
||||
const appInfo = await (window as any).CP2.getAppInfo()
|
||||
if (appInfo.code == 0) return false
|
||||
|
||||
const userInfo = await (window as any).CP2.getUserInfo()
|
||||
if (userInfo.code == 0) {
|
||||
uni.showToast({ title: "获取诗画用户信息失败", icon: "none" })
|
||||
return false
|
||||
} else {
|
||||
const { accountId, sessionId } = userInfo.data
|
||||
return login.doLogin(1, "", accountId, sessionId)
|
||||
// return login.doLogin(1, "", "67186e49d63fea141aad6772", "67186e49d63fea141aad6776")
|
||||
}
|
||||
},
|
||||
|
||||
// 浙政钉登录
|
||||
zhengdingLogin: () => {
|
||||
const params = new URL(window.location.href).searchParams
|
||||
const token = params.get("uabaseToken")
|
||||
if (token) {
|
||||
return login.doLogin(2, token, "", "")
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
// 执行登录操作 - loginType 1:诗画登录 2:浙政钉登录
|
||||
doLogin: async (loginType: number, token: string, accountId: string, sessionId: string) => {
|
||||
const tokenStorage = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
const userStorage = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
switch (loginType) {
|
||||
case 1: // 诗画登录
|
||||
uni.showLoading({ title: "登录中...", mask: true })
|
||||
const response = await userApi.authsh({ accountId, sessionId })
|
||||
|
||||
if (response.code == 0) {
|
||||
storage.saveToken({ accessToken: response.data.accessToken })
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: "登录成功", icon: "none" })
|
||||
|
||||
const { data: userData } = await userApi.userInfo()
|
||||
storage.saveUserInfo(userData)
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
storage.clearUserStorage()
|
||||
if (response.msg == "token已失效") {
|
||||
uni.showToast({ title: "用户凭证已失效,请退出并重新进入浦江县机关事务智慧后勤一体化平台", icon: "none", duration: 6000 })
|
||||
} else {
|
||||
uni.showToast({ title: response.msg || "登录失败", icon: "none" })
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 2: // 浙政钉登录
|
||||
if (isEmpty(tokenStorage) || isEmpty(userStorage)) {
|
||||
try {
|
||||
uni.showLoading({ title: "登录中...", mask: true })
|
||||
const res = await userApi.auth(token)
|
||||
|
||||
if (res.code == 0) {
|
||||
storage.saveToken(res.data)
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: "登录成功", icon: "none" })
|
||||
|
||||
const { data: userData } = await userApi.userInfo()
|
||||
storage.saveUserInfo(userData)
|
||||
} else {
|
||||
storage.clearUserStorage()
|
||||
if (res.msg == "token已失效") {
|
||||
uni.showToast({ title: "用户凭证已失效,请退出并重新进入浦江县机关事务智慧后勤一体化平台", icon: "none", duration: 6000 })
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || "登录失败", icon: "none" })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "登录错误", icon: "none" })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
uni.showToast({ title: "当前登录环境未知", icon: "none" })
|
||||
}
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
logout: async () => {
|
||||
const isLogin = await hooks.useCheckLogin()
|
||||
if (!isLogin) return
|
||||
uni.showLoading({ title: "退出登录中...", mask: true })
|
||||
storage.clearUserStorage()
|
||||
uni.hideLoading()
|
||||
router.jump("/pages/root/login")
|
||||
uni.showToast({ title: "退出登录成功", icon: "none" })
|
||||
},
|
||||
|
||||
// 切换角色
|
||||
changeRole: () => {
|
||||
router.jump("/pages/user/userRole")
|
||||
},
|
||||
}
|
||||
|
||||
// 打开活动核销对话框
|
||||
const actModalShow = (v) => {
|
||||
state.actModalShow = true
|
||||
state.actVal = v
|
||||
}
|
||||
// 确认核销
|
||||
const actModalConfirm = async () => {
|
||||
try {
|
||||
uni.showLoading({ title: "核销中...", mask: true })
|
||||
const { code, msg }: any = await actCancelApi.appCreateActOrder(state.actVal)
|
||||
if (code != 0) return uni.showToast({ title: msg || "核销失败", icon: "none", duration: 4000 })
|
||||
uni.showToast({ title: "核销成功", icon: "success" })
|
||||
state.actModalShow = false
|
||||
state.actVal = {}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "核销失败", icon: "none" })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
// 取消核销
|
||||
const actModalCancel = () => {
|
||||
state.actModalShow = false
|
||||
state.actVal = {}
|
||||
}
|
||||
|
||||
const { scanCodeAction, processUrlScanResult, processBase64ScanResult } = hooks.useScanCode(actModalShow)
|
||||
|
||||
// 扫一扫
|
||||
const appScanCode = async () => {
|
||||
const scanResult = await scanCodeAction()
|
||||
if (!scanResult) {
|
||||
// uni.showToast({ title: "扫码错误", icon: "none" })
|
||||
return
|
||||
}
|
||||
|
||||
// 判断扫码结果
|
||||
if (scanResult.startsWith("http")) {
|
||||
// 扫码结果是url链接
|
||||
processUrlScanResult(scanResult.replace("/#", ""))
|
||||
} else {
|
||||
// 扫码结果是base64字符串
|
||||
processBase64ScanResult(scanResult)
|
||||
}
|
||||
}
|
||||
|
||||
const onTabLogin = () => {
|
||||
router.jump("/pages/root/login")
|
||||
}
|
||||
|
||||
const onTabMenu = async (item: any) => {
|
||||
const isLogin = await hooks.useCheckLogin()
|
||||
if (isLogin) {
|
||||
if (item.path == "/pages/tabbar/index") return uni.switchTab({ url: item.path })
|
||||
switch (item.text) {
|
||||
case "扫一扫":
|
||||
await appScanCode()
|
||||
break
|
||||
default:
|
||||
router.jump(item.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onNavRightClick = () => {
|
||||
router.jump("/pages/setting/index")
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const doLogout = async () => {
|
||||
await login.logout()
|
||||
}
|
||||
|
||||
// 切换角色
|
||||
const doChangeRole = () => {
|
||||
login.changeRole()
|
||||
}
|
||||
|
||||
// 初始化配置菜单
|
||||
const initMenuConfig = () => {
|
||||
// if (isPreview.value) {
|
||||
// state.menuList.push(
|
||||
// { text: "dev-✨导航✨", icon: "/static/icon/service_1.png", path: "/pages/tabbar/index" },
|
||||
// { text: "dev-外来人员就餐", icon: "/static/icon/service_1.png", path: "/pages/apply/foreign-list" },
|
||||
// { text: "dev-跨单位就餐", icon: "/static/icon/service_1.png", path: "/pages/apply/stride-list" },
|
||||
// { text: "dev-包厢预定", icon: "/static/icon/service_4.png", path: "/pages/boxes/apply-list" },
|
||||
// { text: "dev-第三方应用", icon: "/static/icon/login.png", path: "/pages/thirdApp/happyMall" },
|
||||
// { text: "dev-登录", icon: "/static/icon/login.png", path: "/pages/root/login" },
|
||||
// { text: "dev-角色选择", icon: "/static/icon/login.png", path: "/pages/user/userRole" },
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
const app = getApp({ allowDefault: true })
|
||||
app._initTabbar()
|
||||
// 初始化配置菜单
|
||||
initMenuConfig()
|
||||
// 获取应用信息
|
||||
state.appInfo = await getAppInfo()
|
||||
await userApi.userRole()
|
||||
// await isMallMchUser()
|
||||
// await isMchUser()
|
||||
// 获取存储中的用户信息
|
||||
storage.getUserInfo()
|
||||
})
|
||||
onShow(() => {
|
||||
const app = getApp({ allowDefault: true })
|
||||
app._initTabbar()
|
||||
reportPageAccess("pages/tabbar/my")
|
||||
// 获取存储中的用户信息
|
||||
storage.getUserInfo()
|
||||
// 刷新用户
|
||||
app._getUserInfo()
|
||||
})
|
||||
|
||||
|
||||
const isMallMchUser = async () => {
|
||||
const { data } = await userApi.isMallMchUser()
|
||||
if (data) {
|
||||
state.menuList.unshift({ text: "共富商城商品管理", icon: "/static/icon/service_4.png", path: "/pages/prosperity-shop/goods-manage" })
|
||||
}
|
||||
}
|
||||
const isMchUser = async () => {
|
||||
const { data } = await userApi.isMerchant()
|
||||
// 商家查看优惠券列表
|
||||
if (data) {
|
||||
state.menuList.unshift({ text: "查看用户领取的优惠券列表", icon: "/static/icon/service_1.png", path: "/pages/merchant/coupon-list" })
|
||||
}
|
||||
}
|
||||
|
||||
// 联系客服
|
||||
const doPhone = (phone: string) => {
|
||||
hooks.useMakePhoneCall(phone)
|
||||
}
|
||||
|
||||
const createEventId = () => `evt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
|
||||
const reportPageAccess = async (pageCode: string) => {
|
||||
const tokenStorage: any = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
const userStorage: any = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
if (!userStorage?.sysUser?.userId) return
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
await accessStatsApi.track({
|
||||
eventId: createEventId(),
|
||||
pageCode,
|
||||
userId: userStorage?.sysUser?.userId,
|
||||
eventTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
||||
deviceId: tokenStorage?.access_token || "",
|
||||
clientType: systemInfo?.platform || "",
|
||||
osVersion: systemInfo?.system || "",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.service_item:not(:last-child) {
|
||||
padding-bottom: 40rpx;
|
||||
border-bottom: 1px solid #fafafa;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick } from "vue"
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
import { hooks, router, constant } from "@@/index"
|
||||
import { accessStatsApi, merchantApi } from "@@/api"
|
||||
import { filePath } from "@@/utils/file"
|
||||
|
||||
|
||||
const state = reactive<any>({
|
||||
pa: {
|
||||
pagesize: 10,
|
||||
type: "1",
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
const app = getApp({ allowDefault: true })
|
||||
app._initTabbar()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
const app = getApp({ allowDefault: true })
|
||||
app._initTabbar()
|
||||
reportPageAccess("pages/tabbar/canteen")
|
||||
if (pageRef.value) {
|
||||
pageRef.value.reload(false)
|
||||
}
|
||||
})
|
||||
|
||||
const pageRef = ref<any>(null)
|
||||
const pageHook = hooks.usePage(pageRef, merchantApi.page, null, "data.records")
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: constant.storageConstant.TOKEN,
|
||||
USER_INFO: constant.storageConstant.USERINFO,
|
||||
}
|
||||
const query = (page: number, pagesize: number) => {
|
||||
pageHook.startPage(page, pagesize, false, state.pa)
|
||||
}
|
||||
|
||||
const toDetail = async (v: any) => {
|
||||
const isLogin = await hooks.useCheckLogin()
|
||||
if (isLogin) {
|
||||
router.jump("/pages/canteen/detail", { id: v.id })
|
||||
}
|
||||
}
|
||||
|
||||
const createEventId = () => `evt_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
|
||||
const reportPageAccess = async (pageCode: string) => {
|
||||
const tokenStorage: any = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
const userInfo: any = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
if (!userInfo?.sysUser?.userId) return
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
await accessStatsApi.track({
|
||||
eventId: createEventId(),
|
||||
pageCode,
|
||||
userId: userInfo?.sysUser?.userId,
|
||||
eventTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
|
||||
deviceId: tokenStorage?.access_token || "",
|
||||
clientType: systemInfo?.platform || "",
|
||||
osVersion: systemInfo?.system || "",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout title="食堂" bg="/static/page/blue-bg.png">
|
||||
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="10"
|
||||
safe-area-inset-bottom :loading-more-enabled="true" :show-scrollbar="false" @query="query"
|
||||
:auto-clean-list-when-reload="false" :auto-scroll-to-top-when-reload="false" @scroll="pageHook.scroll">
|
||||
<view class="h-20rpx" />
|
||||
<view class="flex-row card p-20rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i" @tap="toDetail(v)">
|
||||
<image class="w-180rpx h-180rpx rounded-12rpx" :src="filePath(v.logoPic)" mode="aspectFill" />
|
||||
<view class="flex-1 flex-col m-l-20rpx">
|
||||
<view class="flex-row items-center justify-between">
|
||||
<view class="flex-row-center sub-text-34">{{ v.name }}</view>
|
||||
<view>
|
||||
<tm-tag size="s">{{ v.deptName }}</tm-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="second-text-24">
|
||||
<view class="second-text-28">营业时间:</view>
|
||||
<view>{{ v.jobTime?.split(",")[0] ? "早:" + v.jobTime?.split(",")[0] : "" }}</view>
|
||||
<view>{{ v.jobTime?.split(",")[1] ? "中:" + v.jobTime?.split(",")[1] : "" }}</view>
|
||||
<view>{{ v.jobTime?.split(",")[2] ? "晚:" + v.jobTime?.split(",")[2] : "" }}</view>
|
||||
</view>
|
||||
<view v-if="v.addressDes" class="m-t-10rpx second-text-28">
|
||||
<text>食堂地址</text>
|
||||
<text class="m-l-10rpx">{{ v.addressDes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<template #bottom>
|
||||
|
||||
</template>
|
||||
</z-paging>
|
||||
</crud-layout>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export type AccessTrackPayload = {
|
||||
eventId: string
|
||||
pageCode: string
|
||||
userId?: number
|
||||
eventTime?: string
|
||||
deviceId?: string
|
||||
clientType?: string
|
||||
osVersion?: string
|
||||
traceId?: string
|
||||
clientIp?: string
|
||||
}
|
||||
|
||||
export default {
|
||||
track: (data: AccessTrackPayload) =>
|
||||
request({
|
||||
url: "/admin/stats/access/track",
|
||||
method: "POST",
|
||||
data,
|
||||
custom: {
|
||||
showError: false,
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* app领取优惠券
|
||||
*/
|
||||
appGet: (data: any) =>
|
||||
request({
|
||||
url: "/wallet/act/cancel-item/app/get",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* app查询用户的唯一优惠券
|
||||
* @param params.actId 活动id
|
||||
*/
|
||||
appFindUnique: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/act/cancel-item/app/find-unique",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
/**
|
||||
* app查询本店优惠券列表
|
||||
* @param params.actStatus 核销类型
|
||||
*/
|
||||
appMchPage: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/act/cancel-item/app/mch/page",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
/**
|
||||
* app创建活动订单
|
||||
* @param data.actId 活动id
|
||||
* @param data.totalPrice 总金额
|
||||
*/
|
||||
appCreateActOrder: (data: any) =>
|
||||
request({
|
||||
url: "/hard/hard-order/app/create-act-order",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 根据公告id查询活动详情
|
||||
* @param data.id 公告id
|
||||
*/
|
||||
getDetailByNoticeId: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/act/act-cancel/app/detail-by-notice-id",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
/**
|
||||
* 根据活动id查询核销活动
|
||||
* @param data.id 活动id
|
||||
*/
|
||||
getDetailById: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/act/act-cancel/app/detail-by-id",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
reportAdView: (data: any) =>
|
||||
request({
|
||||
url: "/integrated/ad-record/app/report-ad-view",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
page: (params: any) =>
|
||||
request({
|
||||
url: "canteen/boxesInfo/appPage",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
detail: (params: any) =>
|
||||
request({
|
||||
url: "canteen/boxesInfo/getInfo",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
apply: (data: any) =>
|
||||
request({
|
||||
url: "canteen/boxesBooking/create",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
cancel: (data: any) =>
|
||||
request({
|
||||
url: "canteen/boxesInfo/cancel",
|
||||
method: "GET",
|
||||
data,
|
||||
}),
|
||||
get: (data: any) =>
|
||||
request({
|
||||
url: "canteen/boxesInfo/get",
|
||||
method: "GET",
|
||||
data,
|
||||
}),
|
||||
MyBookingPage: (data: any) =>
|
||||
request({
|
||||
url: "canteen/boxesInfo/getMyBookingPage",
|
||||
method: "GET",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
page: (params: any) =>
|
||||
request({
|
||||
url: `/admin/user-face/app/page-join`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
del: (params: any) =>
|
||||
request({
|
||||
url: `/admin/user-face/app`,
|
||||
method: "DELETE",
|
||||
params,
|
||||
}),
|
||||
create: (data: any) =>
|
||||
request({
|
||||
url: `/admin/user-face/app`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
get: (params: any) =>
|
||||
request({
|
||||
url: `/admin/user-face/app/detail`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
put: (data: any) =>
|
||||
request({
|
||||
url: `/admin/user-face/app`,
|
||||
method: "PUT",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
page: (data: any) =>
|
||||
request({
|
||||
url: `/canteen/canteenDailyMenu/app/page`,
|
||||
method: "POST",
|
||||
data,
|
||||
params: {
|
||||
current: data.current,
|
||||
size: data.size,
|
||||
},
|
||||
}),
|
||||
classify: (params: any) =>
|
||||
request({
|
||||
url: `/canteen/canteenOnlineCategory/app/getAllSimple`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
product: (params: any) =>
|
||||
request({
|
||||
url: `/canteen/canteenOnlineSpu/app/getCanteenSpuList`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
checkSpuStock: (params) => {
|
||||
return request({
|
||||
url: "/canteen/canteenOnlineSpu/app/checkSpuStock",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export { default as accessStatsApi } from "./access-stats"
|
||||
export { default as actCancelApi } from "./act-cancel"
|
||||
export { default as adApi } from "./ad"
|
||||
export { default as boxesApi } from "./boxes"
|
||||
export { default as faceApi } from "./face"
|
||||
export { default as foodApi } from "./food"
|
||||
export { default as merchantApi } from "./merchant"
|
||||
export { default as orderApi } from "./order"
|
||||
export { default as payApi } from "./pay"
|
||||
export { default as productApi } from "./product"
|
||||
export { default as refundApi } from "./refund"
|
||||
export { default as totalApi } from "./total"
|
||||
export { default as userCarApi } from "./user-car"
|
||||
export { default as userApi } from "./user"
|
||||
export { default as walletApi } from "./wallet"
|
||||
@@ -0,0 +1,39 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
page: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/mch-manage/app/page-join",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
childrenPage: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/mch-manage/app/children-page-join",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
detail: (params: any): Promise<any> =>
|
||||
request({
|
||||
url: "/wallet/mch-manage/app/detail",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
/**
|
||||
* 店铺分类查询
|
||||
*/
|
||||
selectCatgory: (params: any) =>
|
||||
request({
|
||||
url: "/wallet/mch-catgory/app/select",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
/**
|
||||
* 宁波银行登录
|
||||
*/
|
||||
lbLogin: () =>
|
||||
request({
|
||||
url: "/wallet/mch-manage/app/lb-login",
|
||||
method: "GET",
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
// 食堂订单
|
||||
caneenPage: (params: any) =>
|
||||
request({
|
||||
url: `/hard/hard-order/app/caneen-page-join`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
supPage: (params: any) =>
|
||||
request({
|
||||
url: `/hard/hard-order/app/sup-page-join`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
// 共富商城订单
|
||||
farmerSpuPage: (params: any) =>
|
||||
request({
|
||||
url: `/hard/hard-order/app/farmer-page-join`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
detail: (params: any) =>
|
||||
request({
|
||||
url: `/hard/hard-order/app/detail`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
shopCarList: (params: any) =>
|
||||
request({
|
||||
url: `/hard/hard-car/app/order-id`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
getShopCar: (params: any) =>
|
||||
request({
|
||||
url: `/hard/hard-car/app/mch-detail`,
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
updateShopCar: (data: any) =>
|
||||
request({
|
||||
url: `/hard/hard-car/app/save-or-upload`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 预约时间列表
|
||||
*/
|
||||
apptList: () =>
|
||||
request({
|
||||
url: "/hard/hard-appt/app/list",
|
||||
method: "GET",
|
||||
}),
|
||||
/**
|
||||
* 用户确认设备二维码扫一扫订单
|
||||
* @param orderId 订单id
|
||||
* @param walletTypeId 钱包类型ID
|
||||
*/
|
||||
doneHardQrCodeScanOrder: (data: any) =>
|
||||
request({
|
||||
url: "/hard/hard-api/ad/done-hard-qr-code-order",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 创建共富商城预约订单
|
||||
* @body body.carParentId 购物车id
|
||||
* @body body.walletTypeId 钱包类型id
|
||||
* @body body.pickUpId 自提点id
|
||||
*/
|
||||
appCreateFarmerReserveOrder: (data: any) =>
|
||||
request({
|
||||
url: "/hard/hard-order/app/create-farmer-order",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
// 获得支付订单
|
||||
getOrder: (params) => {
|
||||
return request({
|
||||
url: "/pay/order/get",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 提交支付订单
|
||||
submitOrder: (data) => {
|
||||
return request({
|
||||
url: "/pay/order/submit",
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 创建硬件订单
|
||||
createHardwareOrder: (params) => {
|
||||
return request({
|
||||
url: "/hard/hard-order/app/create-order",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 创建线上超市订单
|
||||
createSupermarketOrder: (params) => {
|
||||
return request({
|
||||
url: "/hard/hard-order/app/create-supermarket-order",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 创建二维码订单
|
||||
createQrCodeOrder: (data): any => {
|
||||
return request({
|
||||
url: "/hard/hard-order/app/create-order-scan-qrcode",
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 结算
|
||||
submitHardwareOrder: (data) => {
|
||||
return request({
|
||||
url: "/hardware/order/submit-order",
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
// 退款
|
||||
refundHardwareOrder: (params) => {
|
||||
return request({
|
||||
url: "/hardware/order/refund",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
// 转账
|
||||
transferMoney: (data): Promise<any> => {
|
||||
return request({
|
||||
url: "/wallet/wallet-rule-transaction/app/transfer-accounts",
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
get: (params) => {
|
||||
return request({
|
||||
url: "/canteen/canteenOnlineSpu/app/getCanteenSpuDetails",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
create: (data: any) => {
|
||||
return request({
|
||||
url: "/hard/hard-after-sale/app/create",
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
cancel: (params: any) => {
|
||||
return request({
|
||||
url: "/hard/hard-after-sale/app/cancel",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
get: (params: any) => {
|
||||
return request({
|
||||
url: "/hard/hard-after-sale/app/detail-by-order-id",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
history: (params: any) => {
|
||||
return request({
|
||||
url: "/hard/hard-after-sale/app/history",
|
||||
method: "GET",
|
||||
params,
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 菜品销量按照分类查询
|
||||
*/
|
||||
productByClassify: (data: any): any =>
|
||||
request({
|
||||
url: `/hard/hard-canteen-total-log/app/product-by-classify`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 近一周各个食堂菜品购买统计
|
||||
*/
|
||||
productByCanteen: (data: any): any =>
|
||||
request({
|
||||
url: `/hard/hard-canteen-total-log/app/product-by-canteen`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
*
|
||||
*/
|
||||
productLikeTop: (data: any): any =>
|
||||
request({
|
||||
url: `/hard/hard-canteen-total-log/app/product-like-top`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 申请车
|
||||
*/
|
||||
apply: (data: any) =>
|
||||
request({
|
||||
url: `/admin/sysUserCar/app/create`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 车列表
|
||||
*/
|
||||
page: (data: any) =>
|
||||
request({
|
||||
url: `/admin/sysUserCar/app/page`,
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import base64 from "base64-utf8"
|
||||
|
||||
import request from "@/sheep/utils/request"
|
||||
import { storageConstant } from "@@/constant"
|
||||
|
||||
// 将 Basic Auth 字符串提取为常量
|
||||
const BASIC_AUTH = "Basic " + base64.encode(import.meta.env.VITE_OAUTH2_APP_CLIENT)
|
||||
|
||||
export default {
|
||||
// 获取用户信息
|
||||
userInfo: (): Promise<any> =>
|
||||
request({
|
||||
url: "/admin/user/info",
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
// 检查token
|
||||
refreshToken: (refresh_token: string): Promise<any> => {
|
||||
const grant_type = "refresh_token"
|
||||
const scope = "server"
|
||||
return request({
|
||||
url: "/auth/oauth2/token",
|
||||
header: {
|
||||
skipToken: true,
|
||||
Authorization: BASIC_AUTH,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
method: "POST",
|
||||
params: { refresh_token, grant_type, scope },
|
||||
})
|
||||
},
|
||||
|
||||
// 浙政钉授权登录
|
||||
auth: (token: string): Promise<any> =>
|
||||
request({
|
||||
url: "/thirdparty/thirdPartyOAth2/zzdsso",
|
||||
method: "POST",
|
||||
data: { token },
|
||||
}),
|
||||
|
||||
// 诗画授权登录
|
||||
authsh: (data: { accountId: string; sessionId: string }): Promise<any> =>
|
||||
request({
|
||||
url: "/thirdparty/thirdPartyOAth2/shjnsso",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
// 账号密码登录
|
||||
login: (params: any) => {
|
||||
const { username, password, randomStr, code, grant_type, scope } = params
|
||||
return request({
|
||||
url: `/auth/oauth2/token`,
|
||||
method: "POST",
|
||||
params: {
|
||||
username,
|
||||
password,
|
||||
randomStr,
|
||||
code,
|
||||
grant_type,
|
||||
scope,
|
||||
},
|
||||
data: { password },
|
||||
header: {
|
||||
skipToken: true,
|
||||
Authorization: BASIC_AUTH,
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// 判断是否是合作商家商户角色
|
||||
isMerchant: (): Promise<any> =>
|
||||
request({
|
||||
url: "/admin/role/isMchUser",
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
// 判断是否是共富商城商户角色
|
||||
isMallMchUser: (): Promise<any> =>
|
||||
request({
|
||||
url: "/admin/role/isMallMchUser",
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
// 判断用户移动端角色
|
||||
userRole: () =>
|
||||
request({
|
||||
url: "/admin/role/app/user-role",
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
// 根据手机号发送验证码
|
||||
getMobilePhoneCode: (mobilePhone: string) =>
|
||||
request({
|
||||
url: `/thirdparty/thirdPartyOAth2/loginSms?mobilePhone=${mobilePhone}`,
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
// 发送忘记密码短信验证码
|
||||
sendForgotPasswordSmsCode: (mobile: string) =>
|
||||
request({
|
||||
url: "/admin/user/forgot-password/sms-code",
|
||||
method: "GET",
|
||||
params: { mobile },
|
||||
}),
|
||||
|
||||
// 短信验证码重置密码
|
||||
resetForgotPassword: (data: { mobile: string; smsCode: string; newPassword: string }) =>
|
||||
request({
|
||||
url: "/admin/user/forgot-password/reset",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
// 设备认证
|
||||
verifyDevice: (data: { mobilePhone: string; code: string }) =>
|
||||
request({
|
||||
url: "/thirdparty/thirdPartyOAth2/verify-device",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
// 用户注册
|
||||
register: (data: any) =>
|
||||
request({
|
||||
url: "/admin/register/default-user",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
// 用户申请注销
|
||||
applyCloseAccount: (data: any) =>
|
||||
request({
|
||||
url: "/admin/user/app/apply/close-account",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import request from "@/sheep/utils/request"
|
||||
|
||||
export default {
|
||||
all: (): Promise<any> =>
|
||||
request({
|
||||
url: "/wallet/wallet-manage/app/get-all",
|
||||
method: "GET",
|
||||
}),
|
||||
getQrCode: (params: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-manage/app/get-qr-code",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
detail: (params: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-manage/app/detail",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
bind: (data: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-active/app/bind-card",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
freeze: (data: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-freeze/app/apply-freeze",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
balanceDetail: (params: any) =>
|
||||
request({
|
||||
url: "pay/wallet/get-id",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
transaction: (params: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-transaction/app/page",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
summary: (params: any) =>
|
||||
request({
|
||||
url: "pay/wallet-transaction/get-summary-by-id",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
rechargeList: (params: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-recharge-package/app/list",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
recharge: (data: any) =>
|
||||
request({
|
||||
url: "wallet/wallet-recharge/app/create",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
/**
|
||||
* 是够能够使用智能柜
|
||||
*/
|
||||
isUseDoor: (): Promise<any> =>
|
||||
request({
|
||||
url: "wallet/wallet-manage/app/is-use-door",
|
||||
method: "GET",
|
||||
data: {},
|
||||
}),
|
||||
/**
|
||||
* 自提点分页
|
||||
* @param params.pageNum 页码
|
||||
* @param params.pageSize 每页数量
|
||||
* @param params.mchIds 商户ID列表
|
||||
*/
|
||||
pickUpPage: (params: any): Promise<any> =>
|
||||
request({
|
||||
url: "wallet/mch-pick-up/app/page",
|
||||
method: "GET",
|
||||
params: params,
|
||||
}),
|
||||
/**
|
||||
* 自提点详情
|
||||
* @param params.id 自提点id
|
||||
*/
|
||||
pickUpDetail: (params: any): Promise<any> =>
|
||||
request({
|
||||
url: "wallet/mch-pick-up/app/detail",
|
||||
method: "GET",
|
||||
params: params,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { computed } from "vue"
|
||||
import { router } from "@@/index"
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题",
|
||||
},
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: "#333",
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
showLeft: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const isHistiry = computed(() => {
|
||||
return router.hasHistory()
|
||||
})
|
||||
|
||||
const isTabbar = computed(() => {
|
||||
return router.hasTabbar()
|
||||
})
|
||||
|
||||
function doBackClick() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function getNavTop() {
|
||||
const { statusBarHeight } = uni.getSystemInfoSync()
|
||||
return `${statusBarHeight}px`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="navbar" :style="{ top: getNavTop() }">
|
||||
<view class="navbar_center" v-if="showLeft && !isTabbar" @tap="doBackClick">
|
||||
<text class="sicon-back icon" v-if="isHistiry" />
|
||||
<text class="sicon-home icon" v-else />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&_center {
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
|
||||
border-radius: 28rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
|
||||
&>.icon {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import { computed } from "vue"
|
||||
import { router } from "@@/index"
|
||||
|
||||
const emit = defineEmits(["rightClick"])
|
||||
|
||||
const rightMap = {
|
||||
"setting": "icon-setting",
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题",
|
||||
},
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: "#333",
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
showLeft: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const isHistiry = computed(() => {
|
||||
return router.hasHistory()
|
||||
})
|
||||
|
||||
const isTabbar = computed(() => {
|
||||
return router.hasTabbar()
|
||||
})
|
||||
|
||||
function doBackClick() {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="navbar" :style="{ backgroundColor: bgColor, height: '44px' }">
|
||||
<view class="navbar_left" v-if="showLeft && !isTabbar" @tap="doBackClick">
|
||||
<text class="sicon-back icon" v-if="isHistiry" />
|
||||
<text class="sicon-home icon" v-else />
|
||||
</view>
|
||||
<view class="navbar_title" :style="{ color: titleColor }">{{ props.title }}</view>
|
||||
<view v-if="props.rightIcon" class="navbar_right" @tap="emit('rightClick')">
|
||||
<text :class="['iconfont', rightMap[props.rightIcon]]" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&_left {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
|
||||
border-radius: 28rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&_right {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
|
||||
border-radius: 28rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&_title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
export interface Props {
|
||||
type: "top" | "bottom"
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: "top",
|
||||
bgColor: "transparent",
|
||||
})
|
||||
|
||||
function getHeight() {
|
||||
const { top, bottom } = uni.getSystemInfoSync().safeAreaInsets!
|
||||
if (props.type && props.type === "bottom") {
|
||||
return bottom + "px"
|
||||
}
|
||||
if (props.type && props.type === "top") {
|
||||
return top + "px"
|
||||
}
|
||||
return 22 + "px"
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="safe" :style="{ backgroundColor: props.bgColor, height: getHeight() }"></view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.safe-top {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue"
|
||||
import { onShow } from "@dcloudio/uni-app"
|
||||
import { router } from "@/sheep"
|
||||
|
||||
const props = defineProps<{ modelValue?: boolean }>()
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
const state = reactive<any>({
|
||||
isAgree: false,
|
||||
})
|
||||
|
||||
const isAgree = computed({
|
||||
get: () => (props.modelValue !== undefined ? props.modelValue : state.isAgree),
|
||||
set: (val: boolean) => {
|
||||
if (props.modelValue !== undefined) {
|
||||
emit("update:modelValue", val)
|
||||
} else {
|
||||
state.isAgree = val
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const jumpToContent = ({ id, title }) => {
|
||||
router.jump("/pages/common/content", { id: id, title: title })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="flex items-center justify-center flex-wrap px-20rpx text-center sub-text-24">
|
||||
<tm-checkbox v-model="isAgree" />
|
||||
<text>我已阅读并同意</text>
|
||||
<text class="theme-text-24 color-blue! underline underline-1 underline-solid underline-blue"
|
||||
@click="jumpToContent({ id: 'user-agreement', title: '用户协议' })">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text>和</text>
|
||||
<text class="theme-text-24 color-blue! underline underline-1 underline-solid underline-blue"
|
||||
@click="jumpToContent({ id: 'privacy-policy', title: '隐私政策' })">
|
||||
《隐私政策》
|
||||
</text>
|
||||
<text>
|
||||
并同意授权
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue"
|
||||
import { onShow } from "@dcloudio/uni-app"
|
||||
|
||||
import { router } from "@/sheep"
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: "授权",
|
||||
confirmText: "同意",
|
||||
cancelText: '取消',
|
||||
})
|
||||
|
||||
const emit = defineEmits(["confirm", "close", "cancel"])
|
||||
|
||||
const state = reactive({
|
||||
affirmModalShow: false,
|
||||
})
|
||||
|
||||
const jumpToContent = ({ id, title }) => {
|
||||
router.jump("/pages/common/content", { id: id, title: title })
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
state.affirmModalShow = false
|
||||
emit("close")
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
state.affirmModalShow = true
|
||||
},
|
||||
close: () => {
|
||||
state.affirmModalShow = false
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tm-modal :show="state.affirmModalShow" :title="title" :cancelText="cancelText" :confirmText="confirmText"
|
||||
height="auto" @close="close" @cancel="emit('cancel')" @confirm="emit('confirm')">
|
||||
<view class="px-20rpx bottom-40rpx">
|
||||
<view class="sub-text-26">
|
||||
<text>欢迎使用"浦江县机关事务智慧后勤一体化平台",我们非常重视您的个人相信和隐私保护。您在使用"浦江县机关事务智慧后勤一体化平台"服务之前,清仔细阅读</text>
|
||||
<text class="theme-text-24 color-blue! underline underline-1 underline-solid underline-blue"
|
||||
@click="jumpToContent({ id: 'user-agreement', title: '用户协议' })">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text>和</text>
|
||||
<text class="theme-text-24 color-blue! underline underline-1 underline-solid underline-blue"
|
||||
@click="jumpToContent({ id: 'privacy-policy', title: '隐私政策' })">
|
||||
《隐私政策》
|
||||
</text>
|
||||
<text>
|
||||
{{ confirmText }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="m-t-20rpx second-text-22">
|
||||
<text>如果同意此政策,请点击"{{ confirmText }}"并开始使用我们的产品和服务,我们尽全力保护您的个人信息安全。</text>
|
||||
</view>
|
||||
</view>
|
||||
</tm-modal>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { router } from '@/sheep';
|
||||
|
||||
const jumpToContent = ({ id, title }) => {
|
||||
router.jump("/pages/common/content", { id: id, title: title })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="flex items-center justify-center flex-wrap px-20rpx text-center sub-text-24">
|
||||
<text class="theme-text-24 color-blue! underline underline-1 underline-solid underline-blue"
|
||||
@click="jumpToContent({ id: 'user-agreement', title: '用户协议' })">
|
||||
用户协议
|
||||
</text>
|
||||
<text class="m-x-10rpx" color-blue>|</text>
|
||||
<text class="theme-text-24 color-blue! underline underline-1 underline-solid underline-blue"
|
||||
@click="jumpToContent({ id: 'privacy-policy', title: '隐私政策' })">
|
||||
隐私政策
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue"
|
||||
|
||||
import { useMakePhoneCall } from "@@/hooks"
|
||||
|
||||
const state = reactive<any>({
|
||||
showModal: false,
|
||||
phoneList: ['15557321501']
|
||||
})
|
||||
|
||||
const doServiceText = () => {
|
||||
state.showModal = true
|
||||
}
|
||||
|
||||
const doPhone = (v: string) => {
|
||||
useMakePhoneCall(v)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<view class="w-full second-text-26 text-center mb-2 mt-10" @tap="doServiceText">
|
||||
<text>联系热线</text>
|
||||
</view>
|
||||
<tm-drawer :show="state.showModal" title="联系热线" size="30%" position="bottom" @close="state.showModal = false">
|
||||
<view v-for="(v) in state.phoneList"
|
||||
class="flex items-center justify-between flex-wrap px-24rpx py-16rpx mx-24rpx my-16rpx text-center sub-text-24 border border-solid border-gray-300 rounded-16rpx bg-white"
|
||||
@click="doPhone(v)">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="iconfont icon-kefu1 font-size-50rpx! color-blue"></text>
|
||||
<text
|
||||
class="m-l-15rpx theme-text-28 color-blue! underline underline-1 underline-solid underline-blue">
|
||||
{{ v }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="iconfont icon-kefu color-blue font-size-40rpx!"></text>
|
||||
</view>
|
||||
</tm-drawer>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { watch, reactive, onMounted } from "vue"
|
||||
import { router } from "@@/index"
|
||||
|
||||
export type Props = {
|
||||
tabBarType: "default" | "just-flot-back"
|
||||
title: string
|
||||
scrollHideTabbar: boolean
|
||||
navBgColor: string
|
||||
navBgTitleColor: string
|
||||
navRightIcon?: string,
|
||||
mainIsScroll: boolean
|
||||
bg: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tabBarType: "default",
|
||||
title: "",
|
||||
scrollHideTabbar: true,
|
||||
navBgColor: "transparent",
|
||||
navBgTitleColor: "#333",
|
||||
navRightIcon: undefined,
|
||||
mainIsScroll: false,
|
||||
bg: "",
|
||||
})
|
||||
const emit = defineEmits(["scroll", "navRightClick"])
|
||||
|
||||
const state = reactive({
|
||||
navBgTitleColor_: props.navBgTitleColor,
|
||||
visble: {
|
||||
topNav: true,
|
||||
topSafe: true,
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.navBgTitleColor,
|
||||
(newVal) => {
|
||||
if (!newVal) return
|
||||
state.navBgTitleColor_ = newVal
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initBackStyle()
|
||||
})
|
||||
|
||||
function scroll(e) {
|
||||
// if (!router.hasTabbar()) return
|
||||
// if (!props.scrollHideTabbar) return
|
||||
// const offset = 50
|
||||
// const top = e.target.scrollTop
|
||||
emit("scroll", e)
|
||||
}
|
||||
|
||||
function defaultPage() {
|
||||
const { route } = router.getCurrentPage() as any
|
||||
const page = __ROUTES__.find((it) => it.path == `/${route}`)
|
||||
if (page && page.name) {
|
||||
return page.name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function initBackStyle() {
|
||||
console.log("tabBarType:", props.tabBarType);
|
||||
|
||||
switch (props.tabBarType) {
|
||||
case "default":
|
||||
break
|
||||
case "just-flot-back":
|
||||
state.visble.topSafe = true
|
||||
state.visble.topNav = false
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="layout">
|
||||
<image class="layout_bg" v-if="props.bg" :src="props.bg" mode="aspectFill" />
|
||||
<view class="layout_header">
|
||||
<base-safe-height :bgColor="props.navBgColor" v-if="state.visble.topSafe" type="top" />
|
||||
<base-nav v-if="state.visble.topNav" :titleColor="props.navBgTitleColor" :bgColor="props.navBgColor"
|
||||
:title="props.title || defaultPage()" :rightIcon="props.navRightIcon"
|
||||
@rightClick="emit('navRightClick')" />
|
||||
<base-just-flot-nav :bgColor="props.navBgColor" :titleColor="props.navBgTitleColor"
|
||||
v-if="!state.visble.topNav"></base-just-flot-nav>
|
||||
</view>
|
||||
<view class="layout_main">
|
||||
<view class="layout_main_top">
|
||||
<slot name="main-top"></slot>
|
||||
</view>
|
||||
<scroll-view :scroll-y="mainIsScroll" @scroll="scroll">
|
||||
<slot></slot>
|
||||
</scroll-view>
|
||||
<view class="layout_main_bottom">
|
||||
<slot name="main-bottom"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// #ifdef H5
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
.layout {
|
||||
width: 100%;
|
||||
// #ifndef H5
|
||||
height: 100vh;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
height: 100%;
|
||||
// #endif
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&_bg {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&_header {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
&_main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 99;
|
||||
|
||||
&_top {}
|
||||
|
||||
scroll-view {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
&_bottom {
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&_foot {}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view class="bg-[#f5f5f5] px-1 py-2 rounded-4 fixed bottom-0 left-0 right-0 w-full">
|
||||
<view class="flex justify-end">
|
||||
<image v-show="show" class="w-[50rpx] h-[50rpx] mx-2 mb-1" src="@/static/page/fold.svg"
|
||||
@click="show = !show" />
|
||||
<image v-show="!show" class="w-[50rpx] h-[50rpx] mx-2 mb-1" src="@/static/page/unfold.svg"
|
||||
@click="show = !show" />
|
||||
</view>
|
||||
<view v-show="show" class="flex mb-2.5" v-for="(row, rowIndex) in keyboardLayout" :key="rowIndex">
|
||||
<view
|
||||
class="flex-1 h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
|
||||
v-for="(key, keyIndex) in row" :key="keyIndex" @click="handleKeyPress(key.toString())">
|
||||
{{ key }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-show="show" class="flex mb-0">
|
||||
<view
|
||||
class="flex-1 h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
|
||||
@click="handleKeyPress('.')">
|
||||
.
|
||||
</view>
|
||||
<view
|
||||
class="flex-auto h-[90rpx] flex items-center justify-center bg-white text-2xl text-[#333] mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
|
||||
@click="handleKeyPress('0')">
|
||||
0
|
||||
</view>
|
||||
<view
|
||||
class="flex-1 h-[90rpx] flex items-center justify-center bg-white mx-[5px] rounded-lg cursor-pointer select-none active:bg-[#f0f0f0]"
|
||||
@click="handleDelete">
|
||||
<image class="size-45rpx" src="@/static/page/keyboard-delete.svg" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
const amount = defineModel<string>("modelValue", { default: "" })
|
||||
|
||||
const keyboardLayout = ref([
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"],
|
||||
])
|
||||
const show = ref(true)
|
||||
|
||||
// 按下键盘
|
||||
const handleKeyPress = (key) => {
|
||||
if (key === "." && amount.value.includes(".")) return
|
||||
if (key === "." && !amount.value) {
|
||||
amount.value = "0."
|
||||
return
|
||||
}
|
||||
// 特殊处理:如果当前为0,新输入不是小数点,则替换0
|
||||
if (amount.value == "0" && key != ".") {
|
||||
amount.value = key
|
||||
return
|
||||
}
|
||||
|
||||
// 特殊处理:如果有小数点且已有两位小数,不再添加数字
|
||||
if (amount.value.includes(".")) {
|
||||
const decimalPlaces = amount.value.split(".")[1]
|
||||
if (decimalPlaces && decimalPlaces.length >= 2) return
|
||||
}
|
||||
|
||||
amount.value += key
|
||||
}
|
||||
|
||||
// 点击删除
|
||||
const handleDelete = () => {
|
||||
if (amount.value.length > 0) {
|
||||
amount.value = amount.value.slice(0, -1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<view class="waterfalls-flow">
|
||||
<view
|
||||
v-for="(item, index) in data.column"
|
||||
:key="index"
|
||||
class="waterfalls-flow-column"
|
||||
:id="`waterfalls_flow_column_${index + 1}`"
|
||||
:msg="msg"
|
||||
:style="{ width: w, 'margin-left': index == 0 ? 0 : m }"
|
||||
>
|
||||
<view
|
||||
:class="['column-value', { 'column-value-show': item2.o }]"
|
||||
v-for="(item2, index2) in columnValue(index)"
|
||||
:key="index2"
|
||||
:style="[s1]"
|
||||
@click.stop="wapperClick(item2)"
|
||||
>
|
||||
<view class="inner" v-if="data.seat == 1">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- #ifdef VUE2 -->
|
||||
<slot name="slot{{item2.index}}"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
<slot :name="`slot${item2.index}`"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<slot v-bind="item2"></slot>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<image
|
||||
:class="[
|
||||
'img',
|
||||
{
|
||||
'img-hide': item2[hideImageKey] == true || item2[hideImageKey] == 1,
|
||||
},
|
||||
{ 'img-error': !item2[data.imageKey] },
|
||||
]"
|
||||
:src="item2[data.imageKey]"
|
||||
mode="widthFix"
|
||||
@load="imgLoad(item2, index + 1)"
|
||||
@error="imgError(item2, index + 1)"
|
||||
@click.stop="imageClick(item2)"
|
||||
></image>
|
||||
<view class="inner" v-if="data.seat == 2">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- #ifdef VUE2 -->
|
||||
<slot name="slot{{item2.index}}"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
<slot :name="`slot${item2.index}`"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<slot v-bind="item2"></slot>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
modelValue: Array,
|
||||
column: {
|
||||
// 列的数量
|
||||
type: [String, Number],
|
||||
default: 2,
|
||||
},
|
||||
maxColumn: {
|
||||
// 最大列数
|
||||
type: [String, Number],
|
||||
default: 5,
|
||||
},
|
||||
columnSpace: {
|
||||
// 列之间的间距 百分比
|
||||
type: [String, Number],
|
||||
default: 2,
|
||||
},
|
||||
imageKey: {
|
||||
// 图片key
|
||||
type: [String],
|
||||
default: "image",
|
||||
},
|
||||
hideImageKey: {
|
||||
// 隐藏图片key
|
||||
type: [String],
|
||||
default: "hide",
|
||||
},
|
||||
seat: {
|
||||
// 文本的位置,1图片之上 2图片之下
|
||||
type: [String, Number],
|
||||
default: 2,
|
||||
},
|
||||
listStyle: {
|
||||
// 单个展示项的样式:eg:{'background':'red'}
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: {
|
||||
list: this.modelValue ? this.modelValue : [],
|
||||
column: this.column < 2 ? 2 : this.column,
|
||||
columnSpace: this.columnSpace <= 5 ? this.columnSpace : 5,
|
||||
imageKey: this.imageKey,
|
||||
seat: this.seat,
|
||||
},
|
||||
msg: 0,
|
||||
listInitStyle: {
|
||||
"border-radius": "12rpx",
|
||||
"margin-bottom": "20rpx",
|
||||
"background-color": "#fff",
|
||||
},
|
||||
adds: [], //预置数据
|
||||
isLoaded: true,
|
||||
curIndex: 0,
|
||||
isRefresh: true,
|
||||
flag: false,
|
||||
refreshDatas: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 计算列宽
|
||||
w() {
|
||||
const column_rate = `${100 / this.data.column - +this.data.columnSpace}%`
|
||||
return column_rate
|
||||
},
|
||||
// 计算margin
|
||||
m() {
|
||||
const column_margin = `${
|
||||
(100 - (100 / this.data.column - +this.data.columnSpace).toFixed(5) * this.data.column) / (this.data.column - 1)
|
||||
}%`
|
||||
return column_margin
|
||||
},
|
||||
// list样式
|
||||
s1() {
|
||||
return { ...this.listInitStyle, ...this.listStyle }
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 初始化
|
||||
this.refresh()
|
||||
},
|
||||
methods: {
|
||||
// 预加载图片
|
||||
loadImages(idx = 0) {
|
||||
let count = 0
|
||||
const newList = this.data.list.filter((item, index) => index >= idx)
|
||||
for (let i = 0; i < newList.length; i++) {
|
||||
// #ifndef APP-PLUS
|
||||
uni.getImageInfo({
|
||||
src: `${newList[i][this.imageKey]}.jpg`,
|
||||
complete: (res) => {
|
||||
count++
|
||||
if (count == newList.length) this.initValue(idx)
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.io.getImageInfo({
|
||||
src: `${newList[i][this.imageKey]}.jpg`,
|
||||
complete: (res) => {
|
||||
count++
|
||||
if (count == newList.length) this.initValue(idx)
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
// 刷新
|
||||
refresh() {
|
||||
if (!this.isLoaded) {
|
||||
this.refreshDatas = this.modelValue
|
||||
return false
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.refreshDatas = []
|
||||
this.isRefresh = true
|
||||
this.adds = []
|
||||
this.data.list = this.modelValue ? this.modelValue : []
|
||||
this.data.column = this.column < 2 ? 2 : this.column >= this.maxColumn ? this.maxColumn : this.column
|
||||
this.data.columnSpace = this.columnSpace <= 5 ? this.columnSpace : 5
|
||||
this.data.imageKey = this.imageKey
|
||||
this.data.seat = this.seat
|
||||
this.curIndex = 0
|
||||
// 每列的数据初始化
|
||||
for (let i = 1; i <= this.data.column; i++) {
|
||||
this.data[`column_${i}_values`] = []
|
||||
this.msg++
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.initValue(this.curIndex, "refresh==>")
|
||||
})
|
||||
}, 1)
|
||||
},
|
||||
columnValue(index) {
|
||||
return this.data[`column_${index + 1}_values`]
|
||||
},
|
||||
change(newValue) {
|
||||
for (let i = 0; i < this.data.list.length; i++) {
|
||||
const cv = this.data[`column_${this.data.list[i].column}_values`]
|
||||
for (let j = 0; j < cv.length; j++) {
|
||||
if (newValue[i] && i === cv[j].index) {
|
||||
this.data[`column_${this.data.list[i].column}_values`][j] = Object.assign(cv[j], newValue[i])
|
||||
this.msg++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getMin(a, s) {
|
||||
let m = a[0][s]
|
||||
let mo = a[0]
|
||||
for (var i = a.length - 1; i >= 0; i--) {
|
||||
if (a[i][s] < m) {
|
||||
m = a[i][s]
|
||||
}
|
||||
}
|
||||
mo = a.filter((i) => i[s] == m)
|
||||
return mo[0]
|
||||
},
|
||||
// 计算每列的高度
|
||||
getMinColumnHeight() {
|
||||
return new Promise((resolve) => {
|
||||
const heightArr = []
|
||||
for (let i = 1; i <= this.data.column; i++) {
|
||||
const query = uni.createSelectorQuery().in(this)
|
||||
query
|
||||
.select(`#waterfalls_flow_column_${i}`)
|
||||
.boundingClientRect((data) => {
|
||||
heightArr.push({ column: i, height: data.height })
|
||||
})
|
||||
.exec(() => {
|
||||
if (this.data.column <= heightArr.length) {
|
||||
resolve(this.getMin(heightArr, "height"))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
async initValue(i, from) {
|
||||
this.isLoaded = false
|
||||
if (i >= this.data.list.length || this.refreshDatas.length) {
|
||||
this.msg++
|
||||
this.loaded()
|
||||
return false
|
||||
}
|
||||
const minHeightRes = await this.getMinColumnHeight()
|
||||
const c = this.data[`column_${minHeightRes.column}_values`]
|
||||
this.data.list[i].column = minHeightRes.column
|
||||
c.push({ ...this.data.list[i], cIndex: c.length, index: i, o: 0 })
|
||||
this.msg++
|
||||
},
|
||||
// 图片加载完成
|
||||
imgLoad(item, c) {
|
||||
const i = item.index
|
||||
item.o = 1
|
||||
this.$set(this.data[`column_${c}_values`], item.cIndex, JSON.parse(JSON.stringify(item)))
|
||||
this.initValue(i + 1)
|
||||
},
|
||||
// 图片加载失败
|
||||
imgError(item, c) {
|
||||
const i = item.index
|
||||
item.o = 1
|
||||
item[this.data.imageKey] = null
|
||||
this.$set(this.data[`column_${c}_values`], item.cIndex, JSON.parse(JSON.stringify(item)))
|
||||
this.initValue(i + 1)
|
||||
},
|
||||
// 渲染结束
|
||||
loaded() {
|
||||
if (this.refreshDatas.length) {
|
||||
this.isLoaded = true
|
||||
this.refresh()
|
||||
return false
|
||||
}
|
||||
this.curIndex = this.data.list.length
|
||||
if (this.adds.length) {
|
||||
this.data.list = this.adds[0]
|
||||
this.adds.splice(0, 1)
|
||||
this.initValue(this.curIndex)
|
||||
} else {
|
||||
if (this.data.list.length) this.$emit("loaded")
|
||||
this.isLoaded = true
|
||||
this.isRefresh = false
|
||||
}
|
||||
},
|
||||
// 单项点击事件
|
||||
wapperClick(item) {
|
||||
this.$emit("wapperClick", item)
|
||||
},
|
||||
// 图片点击事件
|
||||
imageClick(item) {
|
||||
this.$emit("imageClick", item)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
deep: true,
|
||||
handler(newValue, oldValue) {
|
||||
setTimeout(() => {
|
||||
this.$nextTick(() => {
|
||||
if (this.isRefresh) return false
|
||||
if (this.isLoaded) {
|
||||
this.refresh()
|
||||
if (newValue.length <= this.curIndex) return this.change(newValue)
|
||||
this.data.list = newValue
|
||||
this.$nextTick(() => {
|
||||
this.initValue(this.curIndex, "watch==>")
|
||||
})
|
||||
} else {
|
||||
this.adds.push(newValue)
|
||||
}
|
||||
})
|
||||
}, 10)
|
||||
},
|
||||
},
|
||||
column(newValue) {
|
||||
this.refresh()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.waterfalls-flow {
|
||||
overflow: hidden;
|
||||
|
||||
&-column {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.column-value {
|
||||
width: 100%;
|
||||
font-size: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.4s;
|
||||
opacity: 0;
|
||||
|
||||
&-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.inner {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
|
||||
&-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-error {
|
||||
background: #f2f2f2
|
||||
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAiAQMAAAAatXkPAAAABlBMVEUAAADMzMzIT8AyAAAAAXRSTlMAQObYZgAAAIZJREFUCNdlzjEKwkAUBNAfEGyCuYBkLyLuxRYW2SKlV1JSeA2tUiZg4YrLjv9PGsHqNTPMSAQuyAJgRDHSyvBPwtZoSJXakeJI9iuRLGDygdl6V0yKDtyMAeMPZySj8yfD+UapvRPj2JOwkyAooSV5IwdDjPdCPspe8LyTl9IKJvDETKKRv6vnlUasgg0fAAAAAElFTkSuQmCC)
|
||||
no-repeat center center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
// z-tabs全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
|
||||
export default {
|
||||
'bg-color': 'transparent',
|
||||
'active-color': '#1777FF',
|
||||
'inactive-color': '#999999'
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
<!-- z-tabs v0.2.5 by-ZXLee -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-tabs -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?name=z-tabs -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<template name="z-tabs">
|
||||
<view class="z-tabs-conatiner" :style="[{ background: bgColor }, tabsStyle]">
|
||||
<view class="z-tabs-left">
|
||||
<slot name="left" />
|
||||
</view>
|
||||
<view ref="z-tabs-scroll-view-conatiner" class="z-tabs-scroll-view-conatiner">
|
||||
<scroll-view ref="z-tabs-scroll-view" class="z-tabs-scroll-view" :scroll-x="true" :scroll-left="scrollLeft"
|
||||
:show-scrollbar="false" :scroll-with-animation="isFirstLoaded" @scroll="scroll">
|
||||
<view class="z-tabs-list-container" :style="[tabsListStyle]">
|
||||
<view class="z-tabs-list" :style="[tabsListStyle, { marginTop: -finalBottomSpace + 'px' }]">
|
||||
<view :ref="`z-tabs-item-${index}`" :id="`z-tabs-item-${index}`" class="z-tabs-item"
|
||||
:style="[tabStyle]" v-for="(item, index) in list" :key="index"
|
||||
@click="tabsClick(index, item)">
|
||||
<view class="z-tabs-item-title-container">
|
||||
<text
|
||||
:class="{ 'z-tabs-item-title': true, 'z-tabs-item-title-disabled': item.disabled }"
|
||||
:style="[{ color: item.disabled ? disabledColor : (currentIndex === index ? activeColor : inactiveColor) }, item.disabled ? disabledStyle : (currentIndex === index ? activeStyle : inactiveStyle)]">
|
||||
{{ item[nameKey] || item }}
|
||||
</text>
|
||||
<text v-if="item.badge && _formatCount(item.badge.count).length"
|
||||
class="z-tabs-item-badge" :style="[badgeStyle]">{{ _formatCount(item.badge.count)
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="z-tabs-bottom"
|
||||
:style="[{ width: tabsContainerWidth + 'px', bottom: finalBottomSpace + 'px' }]">
|
||||
<view ref="z-tabs-bottom-dot" class="z-tabs-bottom-dot" <!-- #ifndef APP-NVUE -->
|
||||
:style="[{transform:`translateX(${bottomDotX}px)`,transition:dotTransition,background:activeColor},finalDotStyle]"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
:style="[{background:activeColor},finalDotStyle]"
|
||||
<!-- #endif -->
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="z-tabs-right">
|
||||
<slot name="right" />
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef APP-NVUE
|
||||
const weexDom = weex.requireModule('dom');
|
||||
const weexAnimation = weex.requireModule('animation');
|
||||
// #endif
|
||||
import zTabsConfig from './config/index'
|
||||
|
||||
//获取默认配置信息
|
||||
function _gc(key, defaultValue) {
|
||||
let config = null;
|
||||
if (zTabsConfig && Object.keys(zTabsConfig).length) {
|
||||
config = zTabsConfig;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
const value = config[_toKebab(key)];
|
||||
return value === undefined ? defaultValue : value;
|
||||
}
|
||||
//驼峰转短横线
|
||||
function _toKebab(value) {
|
||||
return value.replace(/([A-Z])/g, "-$1").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* z-tabs 标签
|
||||
* @description 一个简单轻量的tabs标签,全平台兼容,支持nvue、vue3
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=z-tabs
|
||||
* @property {Array} list 数据源数组,支持形如['tab1','tab2']的格式或[{name:'tab1',value:1}]的格式
|
||||
* @property {Number|String} current 当前选中的index,默认为0
|
||||
* @property {Number|String} scroll-count list数组长度超过scrollCount时滚动显示(不自动铺满全屏),默认为5
|
||||
* @property {Number|String} tab-width 自定义每个tab的宽度,默认为0,即代表根据内容自动撑开,单位rpx,支持传100、"100px"或"100rpx"
|
||||
* @property {Number|String} bar-width 滑块宽度,单位rpx,支持传100、"100px"或"100rpx"
|
||||
* @property {Number|String} bar-height 滑块高度,单位rpx,支持传100、"100px"或"100rpx"
|
||||
* @property {Object} bar-style 滑块样式,其中的width和height将被bar-width和bar-height覆盖
|
||||
* @property {Number|String} bottom-space tabs与底部的间距,单位rpx,支持传100、"100px"或"100rpx"
|
||||
* @property {String} bar-animate-mode 切换tab时滑块动画模式,与swiper联动时有效,点击切换tab时无效,必须调用setDx。默认为line,即切换tab时滑块宽度保持不变,线性运动。可选值为worm,即为类似毛毛虫蠕动效果
|
||||
* @property {String} name-key list中item的name(标题)的key,默认为name
|
||||
* @property {String} value-key list中item的value的key,默认为value
|
||||
* @property {String} active-color 激活状态tab的颜色
|
||||
* @property {String} inactive-color 未激活状态tab的颜色
|
||||
* @property {String} disabled-color 禁用状态tab的颜色
|
||||
* @property {Object} active-style 激活状态tab的样式
|
||||
* @property {Object} inactive-style 未激活状态tab的样式
|
||||
* @property {Object} disabled-style 禁用状态tab的样式
|
||||
* @property {Number|String} badge-max-count 徽标数最大数字限制,超过这个数字将变成badge-max-count+,默认为99
|
||||
* @property {Object} badge-style 徽标样式,例如可自定义背景色,字体等等
|
||||
* @property {String} bg-color z-tabs背景色
|
||||
* @property {Object} tabs-style z-tabs样式
|
||||
* @property {Boolean} init-trigger-change 初始化时是否自动触发change事件
|
||||
* @event {Function(index,value)} change tabs改变时触发,index:当前切换到的index;value:当前切换到的value
|
||||
* @example <z-tabs :list="list"></z-tabs>
|
||||
*/
|
||||
export default {
|
||||
name: 'z-tabs',
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0,
|
||||
currentSwiperIndex: 0,
|
||||
bottomDotX: -1,
|
||||
bottomDotXForIndex: 0,
|
||||
showBottomDot: false,
|
||||
shouldSetDx: true,
|
||||
|
||||
barCalcedWidth: 0,
|
||||
pxBarWidth: 0,
|
||||
scrollLeft: 0,
|
||||
tabsSuperWidth: uni.upx2px(750),
|
||||
tabsWidth: uni.upx2px(750),
|
||||
tabsHeight: uni.upx2px(80),
|
||||
tabsLeft: 0,
|
||||
tabsContainerWidth: 0,
|
||||
itemNodeInfos: [],
|
||||
isFirstLoaded: false,
|
||||
currentScrollLeft: 0,
|
||||
changeTriggerFailed: false,
|
||||
currentChanged: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
//数据源数组,支持形如['tab1','tab2']的格式或[{name:'tab1',value:1}]的格式
|
||||
list: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
//当前选中的index
|
||||
current: {
|
||||
type: [Number, String],
|
||||
default: _gc('current', 0)
|
||||
},
|
||||
//list数组长度超过scrollCount时滚动显示(不自动铺满全屏)
|
||||
scrollCount: {
|
||||
type: [Number, String],
|
||||
default: _gc('scrollCount', 5)
|
||||
},
|
||||
//z-tabs样式
|
||||
tabsStyle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return _gc('tabsStyle', {})
|
||||
}
|
||||
},
|
||||
//自定义每个tab的宽度,默认为0,即代表根据内容自动撑开,单位rpx,支持传100、"100px"或"100rpx"
|
||||
tabWidth: {
|
||||
type: [Number, String],
|
||||
default: _gc('tabWidth', 0)
|
||||
},
|
||||
//滑块宽度,单位rpx,支持传100、"100px"或"100rpx"
|
||||
barWidth: {
|
||||
type: [Number, String],
|
||||
default: _gc('barWidth', 45)
|
||||
},
|
||||
//滑块高度,单位rpx,支持传100、"100px"或"100rpx"
|
||||
barHeight: {
|
||||
type: [Number, String],
|
||||
default: _gc('barHeight', 8)
|
||||
},
|
||||
//滑块样式,其中的width和height将被barWidth和barHeight覆盖
|
||||
barStyle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return _gc('barStyle', {});
|
||||
}
|
||||
},
|
||||
//tabs与底部的间距,单位rpx,支持传100、"100px"或"100rpx"
|
||||
bottomSpace: {
|
||||
type: [Number, String],
|
||||
default: _gc('bottomSpace', 8)
|
||||
},
|
||||
//切换tab时滑块动画模式,与swiper联动时有效,点击切换tab时无效,必须调用setDx。默认为line,即切换tab时滑块宽度保持不变,线性运动。可选值为worm,即为类似毛毛虫蠕动效果
|
||||
barAnimateMode: {
|
||||
type: String,
|
||||
default: _gc('barAnimateMode', 'line')
|
||||
},
|
||||
//list中item的name(标题)的key
|
||||
nameKey: {
|
||||
type: String,
|
||||
default: _gc('nameKey', 'name')
|
||||
},
|
||||
//list中item的value的key
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: _gc('valueKey', 'value')
|
||||
},
|
||||
//激活状态tab的颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: _gc('activeColor', '#007AFF')
|
||||
},
|
||||
//未激活状态tab的颜色
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: _gc('inactiveColor', '#666666')
|
||||
},
|
||||
//禁用状态tab的颜色
|
||||
disabledColor: {
|
||||
type: String,
|
||||
default: _gc('disabledColor', '#bbbbbb')
|
||||
},
|
||||
//激活状态tab的样式
|
||||
activeStyle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return _gc('activeStyle', {});
|
||||
}
|
||||
},
|
||||
//未激活状态tab的样式
|
||||
inactiveStyle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return _gc('inactiveStyle', {});
|
||||
}
|
||||
},
|
||||
//禁用状态tab的样式
|
||||
disabledStyle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return _gc('disabledStyle', {});
|
||||
}
|
||||
},
|
||||
//z-tabs背景色
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: _gc('bgColor', 'white')
|
||||
},
|
||||
//徽标数最大数字限制,超过这个数字将变成badgeMaxCount+
|
||||
badgeMaxCount: {
|
||||
type: [Number, String],
|
||||
default: _gc('badgeMaxCount', 99)
|
||||
},
|
||||
//徽标样式,例如可自定义背景色,字体等等
|
||||
badgeStyle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return _gc('badgeStyle', {})
|
||||
}
|
||||
},
|
||||
//初始化时是否自动触发change事件
|
||||
initTriggerChange: {
|
||||
type: Boolean,
|
||||
default: _gc('initTriggerChange', false)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateSubviewLayout();
|
||||
},
|
||||
watch: {
|
||||
current: {
|
||||
handler(newVal) {
|
||||
this.currentChanged && this._lockDx();
|
||||
this.currentIndex = newVal;
|
||||
this._preUpdateDotPosition(this.currentIndex);
|
||||
if (this.initTriggerChange) {
|
||||
if (newVal < this.list.length) {
|
||||
this.$emit('change', newVal, this.list[newVal][this.valueKey]);
|
||||
} else {
|
||||
this.changeTriggerFailed = true;
|
||||
}
|
||||
}
|
||||
this.currentChanged = true;
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
list: {
|
||||
handler(newVal) {
|
||||
this._handleListChange(newVal);
|
||||
},
|
||||
immediate: false
|
||||
},
|
||||
bottomDotX(newVal) {
|
||||
if (newVal >= 0) {
|
||||
// #ifndef APP-NVUE
|
||||
this.showBottomDot = true;
|
||||
// #endif
|
||||
this.$nextTick(() => {
|
||||
// #ifdef APP-NVUE
|
||||
weexAnimation.transition(this.$refs['z-tabs-bottom-dot'], {
|
||||
styles: {
|
||||
transform: `translateX(${newVal}px)`
|
||||
},
|
||||
duration: this.showAnimate ? 200 : 0,
|
||||
delay: 0
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.showBottomDot = true;
|
||||
}, 10)
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
},
|
||||
finalBarWidth: {
|
||||
handler(newVal) {
|
||||
this.barCalcedWidth = newVal;
|
||||
this.pxBarWidth = this.barCalcedWidth;
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
currentIndex: {
|
||||
handler(newVal) {
|
||||
this.currentSwiperIndex = newVal;
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldScroll() {
|
||||
return this.list.length > this.scrollCount;
|
||||
},
|
||||
finalTabsHeight() {
|
||||
return this.tabsHeight;
|
||||
},
|
||||
tabStyle() {
|
||||
const stl = this.shouldScroll ? { 'flex-shrink': 0 } : { 'flex': 1 };
|
||||
if (this.finalTabWidth > 0) {
|
||||
stl['width'] = this.finalTabWidth + 'px';
|
||||
} else {
|
||||
delete stl.width;
|
||||
}
|
||||
return stl;
|
||||
},
|
||||
tabsListStyle() {
|
||||
return this.shouldScroll ? {} : { 'flex': 1 };
|
||||
},
|
||||
showAnimate() {
|
||||
return this.isFirstLoaded && !this.shouldSetDx;
|
||||
},
|
||||
dotTransition() {
|
||||
return this.showAnimate ? 'transform .2s linear' : 'none';
|
||||
},
|
||||
finalDotStyle() {
|
||||
return { ...this.barStyle, width: this.barCalcedWidth + 'px', height: this.finalBarHeight + 'px', opacity: this.showBottomDot ? 1 : 0 };
|
||||
},
|
||||
finalTabWidth() {
|
||||
return this._convertTextToPx(this.tabWidth);
|
||||
},
|
||||
finalBarWidth() {
|
||||
return this._convertTextToPx(this.barWidth);
|
||||
},
|
||||
finalBarHeight() {
|
||||
return this._convertTextToPx(this.barHeight);
|
||||
},
|
||||
finalBottomSpace() {
|
||||
return this._convertTextToPx(this.bottomSpace);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//根据swiper的@transition实时更新底部dot位置
|
||||
setDx(dx) {
|
||||
if (!this.shouldSetDx) return;
|
||||
const isLineMode = this.barAnimateMode === 'line';
|
||||
const isWormMode = this.barAnimateMode === 'worm';
|
||||
let dxRate = dx / this.tabsSuperWidth;
|
||||
this.currentSwiperIndex = this.currentIndex + parseInt(dxRate);
|
||||
const isRight = dxRate > 0;
|
||||
const barWidth = this.pxBarWidth;
|
||||
if (this.currentSwiperIndex !== this.currentIndex) {
|
||||
dxRate = dxRate - (this.currentSwiperIndex - this.currentIndex);
|
||||
const currentNode = this.itemNodeInfos[this.currentSwiperIndex];
|
||||
if (!!currentNode) {
|
||||
this.bottomDotXForIndex = this._getBottomDotX(currentNode, barWidth);
|
||||
}
|
||||
}
|
||||
const currentIndex = this.currentSwiperIndex;
|
||||
let nextIndex = currentIndex + (isRight ? 1 : -1);
|
||||
nextIndex = Math.max(0, nextIndex);
|
||||
nextIndex = Math.min(nextIndex, this.itemNodeInfos.length - 1);
|
||||
const currentNodeInfo = this.itemNodeInfos[currentIndex];
|
||||
const nextNodeInfo = this.itemNodeInfos[nextIndex];
|
||||
const nextBottomX = this._getBottomDotX(nextNodeInfo, barWidth);
|
||||
if (isLineMode) {
|
||||
this.bottomDotX = this.bottomDotXForIndex + (nextBottomX - this.bottomDotXForIndex) * Math.abs(dxRate);
|
||||
} else if (isWormMode) {
|
||||
if ((isRight && currentIndex >= this.itemNodeInfos.length - 1) || (!isRight && currentIndex <= 0)) return;
|
||||
const spaceOffset = isRight ? nextNodeInfo.right - currentNodeInfo.left : currentNodeInfo.right - nextNodeInfo.left;
|
||||
let barCalcedWidth = barWidth + spaceOffset * Math.abs(dxRate);
|
||||
if (isRight) {
|
||||
if (barCalcedWidth > nextBottomX - this.bottomDotX + barWidth) {
|
||||
const barMinusWidth = barWidth + spaceOffset * (1 - dxRate);
|
||||
this.bottomDotX = this.bottomDotXForIndex + (barCalcedWidth - barMinusWidth) / 2;
|
||||
barCalcedWidth = barMinusWidth;
|
||||
}
|
||||
} else if (!isRight) {
|
||||
if (barCalcedWidth > this.bottomDotXForIndex + barWidth - nextBottomX) {
|
||||
const barMinusWidth = barWidth + spaceOffset * (1 + dxRate);
|
||||
barCalcedWidth = barMinusWidth;
|
||||
this.bottomDotX = nextBottomX;
|
||||
} else {
|
||||
this.bottomDotX = this.bottomDotXForIndex - (barCalcedWidth - barWidth);
|
||||
}
|
||||
}
|
||||
barCalcedWidth = Math.max(barCalcedWidth, barWidth);
|
||||
this.barCalcedWidth = barCalcedWidth;
|
||||
}
|
||||
},
|
||||
//在swiper的@animationfinish中通知z-tabs结束多setDx的锁定,若在父组件中调用了setDx,则必须调用unlockDx
|
||||
unlockDx() {
|
||||
this.$nextTick(() => {
|
||||
this.shouldSetDx = true;
|
||||
})
|
||||
},
|
||||
//更新z-tabs内部布局
|
||||
updateSubviewLayout(tryCount = 0) {
|
||||
this.$nextTick(() => {
|
||||
let delayTime = 10;
|
||||
// #ifdef APP-NVUE || MP-BAIDU
|
||||
delayTime = 50;
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
this._getNodeClientRect('.z-tabs-scroll-view-conatiner').then(res => {
|
||||
if (res) {
|
||||
if (!res[0].width && tryCount < 10) {
|
||||
setTimeout(() => {
|
||||
tryCount++;
|
||||
this.updateSubviewLayout(tryCount);
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
this.tabsWidth = res[0].width;
|
||||
this.tabsHeight = res[0].height;
|
||||
this.tabsLeft = res[0].left;
|
||||
this._handleListChange(this.list);
|
||||
}
|
||||
})
|
||||
this._getNodeClientRect('.z-tabs-conatiner').then(res => {
|
||||
if (res && res[0].width) {
|
||||
this.tabsSuperWidth = res[0].width;
|
||||
}
|
||||
})
|
||||
}, delayTime)
|
||||
})
|
||||
},
|
||||
//点击了tabs
|
||||
tabsClick(index, item) {
|
||||
if (item.disabled) return;
|
||||
if (this.currentIndex != index) {
|
||||
this.shouldSetDx = false;
|
||||
this.$emit('change', index, item[this.valueKey]);
|
||||
this.currentIndex = index;
|
||||
this._preUpdateDotPosition(index);
|
||||
} else {
|
||||
this.$emit('secondClick', index, item[this.valueKey]);
|
||||
}
|
||||
},
|
||||
//scroll-view滚动
|
||||
scroll(e) {
|
||||
this.currentScrollLeft = e.detail.scrollLeft;
|
||||
},
|
||||
//锁定dx,用于避免在swiper被动触发滚动时候执行setDx中的代码
|
||||
_lockDx() {
|
||||
this.shouldSetDx = false;
|
||||
},
|
||||
//更新底部dot位置之前的预处理
|
||||
_preUpdateDotPosition(index) {
|
||||
// #ifndef APP-NVUE
|
||||
this.$nextTick(() => {
|
||||
uni.createSelectorQuery().in(this).select(".z-tabs-scroll-view").fields({
|
||||
scrollOffset: true
|
||||
}, data => {
|
||||
if (data) {
|
||||
this.currentScrollLeft = data.scrollLeft;
|
||||
this._updateDotPosition(index);
|
||||
} else {
|
||||
this._updateDotPosition(index);
|
||||
}
|
||||
}).exec();
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
this._updateDotPosition(index);
|
||||
// #endif
|
||||
},
|
||||
//更新底部dot位置
|
||||
_updateDotPosition(index) {
|
||||
if (index >= this.itemNodeInfos.length) return;
|
||||
this.$nextTick(async () => {
|
||||
let node = this.itemNodeInfos[index];
|
||||
let offset = 0;
|
||||
let tabsContainerWidth = this.tabsContainerWidth;
|
||||
if (JSON.stringify(this.activeStyle) !== '{}') {
|
||||
const nodeRes = await this._getNodeClientRect(`#z-tabs-item-${index}`, true);
|
||||
if (nodeRes) {
|
||||
node = nodeRes[0];
|
||||
offset = this.currentScrollLeft;
|
||||
this.tabsHeight = Math.max(node.height + uni.upx2px(28), this.tabsHeight);
|
||||
tabsContainerWidth = 0;
|
||||
for (let i = 0; i < this.itemNodeInfos.length; i++) {
|
||||
let oldNode = this.itemNodeInfos[i];
|
||||
tabsContainerWidth += i === index ? node.width : oldNode.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.bottomDotX = this._getBottomDotX(node, this.finalBarWidth, offset);
|
||||
this.bottomDotXForIndex = this.bottomDotX;
|
||||
if (this.tabsWidth) {
|
||||
setTimeout(() => {
|
||||
let scrollLeft = this.bottomDotX - this.tabsWidth / 2 + this.finalBarWidth / 2;
|
||||
scrollLeft = Math.max(0, scrollLeft);
|
||||
if (tabsContainerWidth) {
|
||||
scrollLeft = Math.min(scrollLeft, tabsContainerWidth - this.tabsWidth + 10);
|
||||
}
|
||||
if (this.shouldScroll && tabsContainerWidth > this.tabsWidth) {
|
||||
this.scrollLeft = scrollLeft;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.isFirstLoaded = true;
|
||||
})
|
||||
}, 200)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 处理list改变
|
||||
_handleListChange(newVal) {
|
||||
this.$nextTick(async () => {
|
||||
if (newVal.length) {
|
||||
let itemNodeInfos = [];
|
||||
let tabsContainerWidth = 0;
|
||||
let delayTime = 0;
|
||||
// #ifdef MP-BAIDU
|
||||
delayTime = 100;
|
||||
// #endif
|
||||
setTimeout(async () => {
|
||||
for (let i = 0; i < newVal.length; i++) {
|
||||
const nodeRes = await this._getNodeClientRect(`#z-tabs-item-${i}`, true);
|
||||
if (nodeRes) {
|
||||
const node = nodeRes[0];
|
||||
node.left += this.currentScrollLeft;
|
||||
itemNodeInfos.push(node);
|
||||
tabsContainerWidth += node.width;
|
||||
}
|
||||
if (i === this.currentIndex) {
|
||||
this.itemNodeInfos = itemNodeInfos;
|
||||
this.tabsContainerWidth = tabsContainerWidth;
|
||||
this._updateDotPosition(this.currentIndex);
|
||||
}
|
||||
}
|
||||
this.itemNodeInfos = itemNodeInfos;
|
||||
this.tabsContainerWidth = tabsContainerWidth;
|
||||
this._updateDotPosition(this.currentIndex);
|
||||
}, delayTime)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.initTriggerChange && this.changeTriggerFailed && newVal.length) {
|
||||
if (this.current < newVal.length) {
|
||||
this.$emit('change', this.current, newVal[this.current][this.valueKey]);
|
||||
}
|
||||
}
|
||||
},
|
||||
//根据node获取bottomX
|
||||
_getBottomDotX(node, barWidth = this.finalBarWidth, offset = 0) {
|
||||
return node.left + node.width / 2 - barWidth / 2 + offset - this.tabsLeft;
|
||||
},
|
||||
//获取节点信息
|
||||
_getNodeClientRect(select, withRefArr = false) {
|
||||
// #ifdef APP-NVUE
|
||||
select = select.replace('.', '').replace('#', '');
|
||||
const ref = withRefArr ? this.$refs[select][0] : this.$refs[select];
|
||||
return new Promise((resolve, reject) => {
|
||||
if (ref) {
|
||||
weexDom.getComponentRect(ref, option => {
|
||||
if (option && option.result) {
|
||||
resolve([option.size]);
|
||||
} else resolve(false);
|
||||
})
|
||||
} else resolve(false);
|
||||
});
|
||||
return;
|
||||
// #endif
|
||||
const res = uni.createSelectorQuery().in(this);
|
||||
res.select(select).boundingClientRect();
|
||||
return new Promise((resolve, reject) => {
|
||||
res.exec(data => {
|
||||
resolve((data && data != '' && data != undefined && data.length) ? data : false);
|
||||
});
|
||||
});
|
||||
},
|
||||
//格式化badge中的count
|
||||
_formatCount(count) {
|
||||
if (!count) return '';
|
||||
if (count > this.badgeMaxCount) {
|
||||
return this.badgeMaxCount + '+';
|
||||
}
|
||||
return count.toString();
|
||||
},
|
||||
//将文本的px或者rpx转为px的值
|
||||
_convertTextToPx(text) {
|
||||
const dataType = Object.prototype.toString.call(text);
|
||||
if (dataType === '[object Number]') {
|
||||
return uni.upx2px(text);
|
||||
}
|
||||
let isRpx = false;
|
||||
if (text.indexOf('rpx') !== -1 || text.indexOf('upx') !== -1) {
|
||||
text = text.replace('rpx', '').replace('upx', '');
|
||||
isRpx = true;
|
||||
} else if (text.indexOf('px') !== -1) {
|
||||
text = text.replace('px', '');
|
||||
} else {
|
||||
text = uni.upx2px(text);
|
||||
}
|
||||
if (!isNaN(text)) {
|
||||
if (isRpx) return Number(uni.upx2px(text));
|
||||
return Number(text);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.z-tabs-conatiner {
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
width: 750rpx;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.z-tabs-scroll-view-conatiner {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
.z-tabs-scroll-view ::-webkit-scrollbar {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
.z-tabs-scroll-view {
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.z-tabs-list-container {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.z-tabs-list,
|
||||
.z-tabs-list-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.z-tabs-item {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0px 20rpx;
|
||||
}
|
||||
|
||||
.z-tabs-item-title-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.z-tabs-item-title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.z-tabs-item-title-disabled {
|
||||
/* #ifndef APP-NVUE */
|
||||
cursor: not-allowed;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.z-tabs-item-badge {
|
||||
margin-left: 8rpx;
|
||||
background-color: #ec5b56;
|
||||
color: white;
|
||||
font-size: 22rpx;
|
||||
border-radius: 100px;
|
||||
padding: 0rpx 10rpx;
|
||||
}
|
||||
|
||||
.z-tabs-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.z-tabs-bottom-dot {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.z-tabs-left,
|
||||
.z-tabs-right {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
// 微信小程序
|
||||
WECHAT: "wechat",
|
||||
// 支付宝
|
||||
ALI_PAY: "ali_pay",
|
||||
} as const
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as serviceEnum } from "./service-enum"
|
||||
|
||||
export { default as storageConstant } from "./storage-constant"
|
||||
|
||||
export { default as APPENV } from "./app-env"
|
||||
@@ -0,0 +1,67 @@
|
||||
const hardwareOrderStatusEnum = {
|
||||
1: "创建订单(未支付)",
|
||||
5: "已经支付",
|
||||
10: "退款",
|
||||
15: "已核销",
|
||||
}
|
||||
|
||||
const hardwareOrderTypeEnum = {
|
||||
1: "线下购买",
|
||||
2: "线上订餐",
|
||||
3: "外来人员",
|
||||
4: "跨单位",
|
||||
}
|
||||
|
||||
const hardwarePayTypeEnum = {
|
||||
1: "钱包支付",
|
||||
2: "钱包二维码支付",
|
||||
3: "钱包饭卡支付",
|
||||
4: "人脸识别支付",
|
||||
5: "跨单位二维码支付",
|
||||
6: "外来人员二维码支付",
|
||||
7: "线上订餐二维码支付",
|
||||
}
|
||||
|
||||
const hardwareSaleStatusEnum = {
|
||||
10: "会员申请退款",
|
||||
50: "商家确认退款",
|
||||
61: "会员取消退款",
|
||||
62: "商家拒绝退款",
|
||||
}
|
||||
|
||||
const hardwareRefundStatusEnum = {
|
||||
0: "未售后",
|
||||
10: "售后",
|
||||
20: "售后成功",
|
||||
30: "售后失败",
|
||||
}
|
||||
|
||||
const carTypeEnum = {
|
||||
1: "私家车",
|
||||
2: "公车",
|
||||
3: "军车",
|
||||
4: "警车",
|
||||
}
|
||||
|
||||
const boxesBxStatusEnum = {
|
||||
"0": "今日空闲",
|
||||
"1": "今日部分空闲",
|
||||
"2": "今日已满",
|
||||
}
|
||||
|
||||
const actCancelStatusEnum = {
|
||||
"1": "未核销",
|
||||
"2": "已核销",
|
||||
"3": "已过期",
|
||||
}
|
||||
|
||||
export default {
|
||||
hardwareOrderStatusEnum,
|
||||
hardwareOrderTypeEnum,
|
||||
hardwarePayTypeEnum,
|
||||
boxesBxStatusEnum,
|
||||
hardwareSaleStatusEnum,
|
||||
hardwareRefundStatusEnum,
|
||||
carTypeEnum,
|
||||
actCancelStatusEnum,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
TOKEN: "token",
|
||||
USERINFO: "user_info",
|
||||
USERROLE: "userRole",
|
||||
FIRST_AFFIRM: "first_affirm",
|
||||
VERSION_OBJ: "version_obj",
|
||||
USER_SETTING: "user_setting",
|
||||
REMEMBER_USER: "remember_user",
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./use-copy"
|
||||
export * from "./use-enum"
|
||||
export * from "./use-first-affirm"
|
||||
export * from "./use-img"
|
||||
export * from "./use-page"
|
||||
export * from "./use-phone"
|
||||
export * from "./use-scan-code"
|
||||
export * from "./use-token"
|
||||
export * from "./use-unit"
|
||||
@@ -0,0 +1,5 @@
|
||||
export const useCopy = async (str: string | undefined) => {
|
||||
if (str == undefined) return;
|
||||
const res = await uni.setClipboardData({ data: str });
|
||||
uni.showToast({ title: "复制成功", icon: "none" });
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isEmpty } from "radash";
|
||||
|
||||
import { serviceEnum } from "../constant/index";
|
||||
|
||||
export const useEnum = (
|
||||
key: string | undefined,
|
||||
value: string | number | undefined
|
||||
) => {
|
||||
if (isEmpty(key) || isEmpty(value)) return "";
|
||||
return serviceEnum[key!][value];
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ref } from "vue"
|
||||
import { onShow } from "@dcloudio/uni-app"
|
||||
|
||||
import CONSTANT from "@@/constant/storage-constant"
|
||||
|
||||
/**
|
||||
* 第一次同意APP的隐私政策
|
||||
* 要和`affirm-modal`组件配合使用
|
||||
*/
|
||||
export const useFirstAffirm = (showAffirmModal) => {
|
||||
const firstAffirm = ref(false)
|
||||
|
||||
onShow(() => {
|
||||
const storage = uni.getStorageSync(CONSTANT.FIRST_AFFIRM)
|
||||
if (storage && storage != "") {
|
||||
firstAffirm.value = true
|
||||
} else {
|
||||
firstAffirm.value = false
|
||||
}
|
||||
if (!firstAffirm.value) {
|
||||
showAffirmModal?.()
|
||||
}
|
||||
})
|
||||
|
||||
const agree = () => {
|
||||
uni.setStorageSync(CONSTANT.FIRST_AFFIRM, 1)
|
||||
firstAffirm.value = true
|
||||
}
|
||||
|
||||
const reject = () => {
|
||||
uni.removeStorageSync(CONSTANT.FIRST_AFFIRM)
|
||||
firstAffirm.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
firstAffirm,
|
||||
agree,
|
||||
reject,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const usePreviewImage = (str: string | undefined) => {
|
||||
if (str == undefined) return
|
||||
let urlList = str.split(",")
|
||||
uni.previewImage({
|
||||
urls: urlList,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { reactive, onMounted } from "vue"
|
||||
|
||||
export const usePage = (ref: any = null, fn: Function | null = null, sticky = null, struct = "data") => {
|
||||
const state = reactive<any>({
|
||||
loading: true,
|
||||
list: [],
|
||||
headerHeight: 0,
|
||||
scrollable: true,
|
||||
})
|
||||
const scroll = (e: any) => {
|
||||
const scrollTop = e.detail.scrollTop
|
||||
if (scrollTop < state.headerHeight) {
|
||||
state.scrollable = true
|
||||
} else {
|
||||
state.scrollable = false
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
if (sticky != null) {
|
||||
const query = uni.createSelectorQuery()
|
||||
query
|
||||
.select(sticky)
|
||||
.boundingClientRect((data) => {
|
||||
state.headerHeight = (data as any).height
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
})
|
||||
const setPage = async (list: Array<any>) => {
|
||||
if (ref == null) return
|
||||
state.list.totalList = await ref.value.complete(list)
|
||||
}
|
||||
const startPage = async (page: number, pagesize: number, isPage = false, query = {}) => {
|
||||
if (!fn || typeof fn !== "function") {
|
||||
console.warn("startPage 方法请传入 pageFunc 参数 模拟5条假数据")
|
||||
state.list = Array.from(new Array(5))
|
||||
setPage(state.list)
|
||||
state.startPage = startPage
|
||||
state.scroll = scroll
|
||||
return state
|
||||
}
|
||||
const _data = {
|
||||
current: page,
|
||||
size: pagesize,
|
||||
...query,
|
||||
}
|
||||
try {
|
||||
const res = await fn(_data)
|
||||
if (!res) {
|
||||
setPage([])
|
||||
return
|
||||
}
|
||||
let _arr = res
|
||||
for (let key of struct.split(".")) {
|
||||
_arr = _arr[key]
|
||||
}
|
||||
setPage(_arr)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setPage([])
|
||||
}
|
||||
setTimeout((_) => {
|
||||
if (state.loading == true) {
|
||||
state.loading = false
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
state.startPage = startPage
|
||||
state.scroll = scroll
|
||||
return state
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const useMakePhoneCall = (str: string | undefined) => {
|
||||
if (str == undefined) return;
|
||||
uni.makePhoneCall({ phoneNumber: str });
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { router } from ".."
|
||||
import { APPENV } from "../constant"
|
||||
import { getAppInfo, isDevEnv } from "../utils/app"
|
||||
|
||||
export function useScanCode(actModalShow) {
|
||||
async function scanCodeAction(): Promise<string | undefined> {
|
||||
const appInfo = await getAppInfo()
|
||||
if (isDevEnv() && appInfo != APPENV.APP) {
|
||||
// 开发环境模拟扫码
|
||||
// const testQrCode = "eyJpZCI6IjE5MDU4ODg4NjQ0NTQ1MzMxMjEiLCJuYW1lIjoi6ICB5Y6/5bqc6aOf5aCCIn0=" // 测试线上-老县府食堂-收款二维码
|
||||
// const testQrCode = "eyJoYXJkRG9vcklkIjoiMTkxNDIxNTYwODQzNzQxMTg0MSIsImNhYmluZXRObyI6IjEifQ==" // 测试线上-智能柜-收款二维码
|
||||
// const testQrCode = "eyJpZCI6IjE5MDU4MTY3NTEwNDM0NTI5MzAiLCJuYW1lIjoi5paw5Y6/5bqc6LaF5biCIn0=" // 邓本地-测试环境-新县府超市收款二维码
|
||||
// const testQrCode = "eyJoYXJkRG9vcklkIjoiMTkxMzE1NzA4MDQxODI5NTgwOSIsImNhYmluZXRObyI6IjEifQ==" // 邓本地-测试单门智能柜柜门二维码
|
||||
// const testQrCode = "http://192.168.101.45:8889/h5/#/pages/pay/scanPay?id=1905816751043452930&name=测试新县府超市" // 邓本地-测试环境-新县府超市收款二维码
|
||||
// const testQrCode = "http://192.168.101.45:8889/h5/#/pages/pay/scanPay?hardDoorId=1913157080418295809&cabinetNo=1" // 测试智能柜柜门二维码
|
||||
const testQrCode = "eyJjYW5jZWxJZCI6IjE5ODQyNDA4MzM5NDMyMDc5MzcifQ==" // 测试商户活动核销
|
||||
uni.showModal({ content: "开发环境不是浙政钉和诗画环境,扫码仅供测试" })
|
||||
return Promise.resolve(testQrCode)
|
||||
}
|
||||
if (appInfo == APPENV.APP) {
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
uni.scanCode({
|
||||
success: (res) => {
|
||||
console.log("APP扫码成功:", JSON.stringify(res))
|
||||
resolve(res.result || "")
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("APP扫码失败", JSON.stringify(err))
|
||||
resolve("")
|
||||
},
|
||||
})
|
||||
})
|
||||
return scanResult
|
||||
}
|
||||
if (appInfo == APPENV.ZZD) {
|
||||
// 浙政钉扫码
|
||||
const res: any = await (window as any).dd.scan({ type: "all" })
|
||||
return res.text || ""
|
||||
}
|
||||
if (appInfo == APPENV.SH) {
|
||||
// 诗画扫码
|
||||
const res = await (window as any).CP2.showScanQr({ needResult: 1 })
|
||||
if (res.code == 1) return res.data.qrCode || ""
|
||||
return ""
|
||||
}
|
||||
uni.showToast({ title: "当前环境未知", icon: "none" })
|
||||
}
|
||||
|
||||
// 处理url扫码结果
|
||||
function processUrlScanResult(url: string) {
|
||||
if (!url) return
|
||||
const searchParams = new URL(url).searchParams
|
||||
const paramsObj: Record<string, string> = {}
|
||||
searchParams.forEach((value, key) => (paramsObj[key] = value))
|
||||
const { hardDoorId, cabinetNo, action, id, name } = paramsObj
|
||||
|
||||
// 智能柜扫码
|
||||
if (hardDoorId && cabinetNo) {
|
||||
router.jump("/pages/cabinet/buy", { hardDoorId, cabinetNo })
|
||||
return
|
||||
}
|
||||
// 扫收款机上生成的码支付
|
||||
if (action === "harQrCodeScanPay") {
|
||||
const { mchId, orderId } = paramsObj
|
||||
router.jump("/pages/pay/scanPay", { action, mchId, orderId })
|
||||
return
|
||||
}
|
||||
// 扫商户url收款码
|
||||
if (id) {
|
||||
router.jump("/pages/pay/scanPay", { id, name })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" })
|
||||
}
|
||||
|
||||
// 处理base64扫码结果
|
||||
function processBase64ScanResult(scanResult: string) {
|
||||
const decodedString = decodeBase64(scanResult)
|
||||
if (!decodedString) return
|
||||
try {
|
||||
const decodedJson = JSON.parse(decodedString)
|
||||
// 商户活动优惠券扫码核销
|
||||
if (decodedJson.cancelId) {
|
||||
actModalShow({ cancelId: decodedJson.cancelId })
|
||||
return
|
||||
}
|
||||
// 商户扫码支付
|
||||
if (decodedJson.id) {
|
||||
router.jump("/pages/pay/scanPay", { id: decodedJson.id })
|
||||
return
|
||||
}
|
||||
// 智能柜扫码开门
|
||||
if (decodedJson.hardDoorId && decodedJson.cabinetNo) {
|
||||
router.jump("/pages/cabinet/buy", { hardDoorId: decodedJson.hardDoorId, cabinetNo: decodedJson.cabinetNo })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" })
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "无效的二维码内容", icon: "none" })
|
||||
}
|
||||
}
|
||||
|
||||
// 解码base64
|
||||
function decodeBase64(base64String: string): string | undefined {
|
||||
try {
|
||||
const decodedBinaryString = atob(base64String)
|
||||
const uint8Array = new Uint8Array(decodedBinaryString.length)
|
||||
for (let i = 0; i < decodedBinaryString.length; i++) {
|
||||
uint8Array[i] = decodedBinaryString.charCodeAt(i)
|
||||
}
|
||||
const decoder = new TextDecoder("utf-8")
|
||||
const decodedString = decoder.decode(uint8Array)
|
||||
|
||||
// console.log("解码结果", decodedString)
|
||||
|
||||
return decodedString
|
||||
} catch (error) {
|
||||
console.error("Base64 解码失败:", error)
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" })
|
||||
}
|
||||
}
|
||||
|
||||
return { scanCodeAction, processUrlScanResult, processBase64ScanResult }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import storageConstant from "../constant/storage-constant"
|
||||
import { jump } from "../router/index"
|
||||
import { isDevEnv } from "../utils/app"
|
||||
|
||||
export const useToken = () => {
|
||||
const tokenKey = storageConstant.TOKEN
|
||||
const tokenStorage = uni.getStorageSync(tokenKey)
|
||||
return tokenStorage?.access_token ? `Bearer ${tokenStorage.access_token}` : ""
|
||||
}
|
||||
|
||||
export const useDefaultHeader = () => {
|
||||
const token = useToken()
|
||||
return {
|
||||
"TENANT-ID": "1",
|
||||
"CLIENT-TOC": "Y",
|
||||
Authorization: token,
|
||||
}
|
||||
}
|
||||
|
||||
export const useStaticCheckLogin = () => {
|
||||
const tokenKey = storageConstant.TOKEN
|
||||
const tokenStorage = uni.getStorageSync(tokenKey)
|
||||
if (tokenStorage) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export const useCheckLogin = async (): Promise<boolean> => {
|
||||
const tokenKey = storageConstant.TOKEN
|
||||
const tokenStorage = uni.getStorageSync(tokenKey)
|
||||
|
||||
if (tokenStorage) return true
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "您还未登录,请先登录",
|
||||
confirmColor: "#0094ff",
|
||||
cancelText: "取消",
|
||||
confirmText: "去登录",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
jump("/pages/root/login", {}, 3)
|
||||
return
|
||||
}
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
return
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export const useMoney = (num) => {
|
||||
if (typeof num === "undefined") return "0.00"
|
||||
const parsedNumber = typeof num === "string" ? parseFloat(num) : num
|
||||
return (parsedNumber / 100.0).toFixed(2)
|
||||
}
|
||||
|
||||
export function useFormatNum(prefix: string, type: "exact" | string, num: number) {
|
||||
num = num || 0
|
||||
if (type === "exact") {
|
||||
return prefix + num
|
||||
}
|
||||
if (num < 10) {
|
||||
return `${prefix}≤10`
|
||||
}
|
||||
// 情况三:大于 10,除第一位外,其它位都显示为0
|
||||
// 例如:100 - 199 显示为 100+
|
||||
// 9000 - 9999 显示为 9000+
|
||||
const numStr = num.toString()
|
||||
const first = numStr[0]
|
||||
const other = "0".repeat(numStr.length - 1)
|
||||
return `${prefix}${first}${other}+`
|
||||
}
|
||||
|
||||
export function useTime(time, pattern) {
|
||||
if (arguments.length === 0 || !time) {
|
||||
return null
|
||||
}
|
||||
const format = pattern || "{y}-{m}-{d} {h}:{i}:{s}"
|
||||
let date
|
||||
if (typeof time === "object") {
|
||||
date = time
|
||||
} else {
|
||||
if (typeof time === "string" && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time)
|
||||
} else if (typeof time === "string") {
|
||||
time = time
|
||||
.replace(new RegExp(/-/gm), "/")
|
||||
.replace("T", " ")
|
||||
.replace(new RegExp(/\.[\d]{3}/gm), "")
|
||||
}
|
||||
if (typeof time === "number" && time.toString().length === 10) {
|
||||
time = time * 1000
|
||||
}
|
||||
date = new Date(time)
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay(),
|
||||
}
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key]
|
||||
if (key === "a") {
|
||||
return ["日", "一", "二", "三", "四", "五", "六"][value]
|
||||
}
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = "0" + value
|
||||
}
|
||||
return value || 0
|
||||
})
|
||||
return time_str
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "./plugs/index"
|
||||
|
||||
export * as router from "./router"
|
||||
|
||||
export * as constant from "./constant"
|
||||
|
||||
export * as hooks from "./hooks"
|
||||
@@ -0,0 +1 @@
|
||||
export { useRouter } from "./use-router/vite-router-plug"
|
||||
@@ -0,0 +1,78 @@
|
||||
const singleComment = Symbol('singleComment');
|
||||
const multiComment = Symbol('multiComment');
|
||||
|
||||
const stripWithoutWhitespace = () => '';
|
||||
const stripWithWhitespace = (string: any, start: any = "", end: any) => string.slice(start, end).replace(/\S/g, ' ');
|
||||
|
||||
const isEscaped = (jsonString: any, quotePosition: any) => {
|
||||
let index = quotePosition - 1;
|
||||
let backslashCount = 0;
|
||||
|
||||
while (jsonString[index] === '\\') {
|
||||
index -= 1;
|
||||
backslashCount += 1;
|
||||
}
|
||||
|
||||
return Boolean(backslashCount % 2);
|
||||
};
|
||||
|
||||
export default function stripJsonComments(jsonString: any, { whitespace = true } = {}) {
|
||||
if (typeof jsonString !== 'string') {
|
||||
throw new TypeError(
|
||||
`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``,
|
||||
);
|
||||
}
|
||||
|
||||
const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace;
|
||||
|
||||
let isInsideString = false;
|
||||
let isInsideComment: any = false;
|
||||
let offset = 0;
|
||||
let result = '';
|
||||
|
||||
for (let index = 0; index < jsonString.length; index++) {
|
||||
const currentCharacter = jsonString[index];
|
||||
const nextCharacter = jsonString[index + 1];
|
||||
|
||||
if (!isInsideComment && currentCharacter === '"') {
|
||||
const escaped = isEscaped(jsonString, index);
|
||||
if (!escaped) {
|
||||
isInsideString = !isInsideString;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInsideString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isInsideComment && currentCharacter + nextCharacter === '//') {
|
||||
result += jsonString.slice(offset, index);
|
||||
offset = index;
|
||||
isInsideComment = singleComment;
|
||||
index++;
|
||||
} else if (isInsideComment === singleComment && currentCharacter + nextCharacter === '\r\n') {
|
||||
index++;
|
||||
isInsideComment = false;
|
||||
result += strip(jsonString, offset, index);
|
||||
offset = index;
|
||||
continue;
|
||||
} else if (isInsideComment === singleComment && currentCharacter === '\n') {
|
||||
isInsideComment = false;
|
||||
result += strip(jsonString, offset, index);
|
||||
offset = index;
|
||||
} else if (!isInsideComment && currentCharacter + nextCharacter === '/*') {
|
||||
result += jsonString.slice(offset, index);
|
||||
offset = index;
|
||||
isInsideComment = multiComment;
|
||||
index++;
|
||||
continue;
|
||||
} else if (isInsideComment === multiComment && currentCharacter + nextCharacter === '*/') {
|
||||
index++;
|
||||
isInsideComment = false;
|
||||
result += strip(jsonString, offset, index + 1);
|
||||
offset = index + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return result + (isInsideComment ? (strip as any)(jsonString.slice(offset)) : jsonString.slice(offset));
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import fs from "fs"
|
||||
import stripJsonComments from "./strip-json-comments"
|
||||
|
||||
class TransformPages {
|
||||
routes: any = []
|
||||
tabbar: any = []
|
||||
includes = []
|
||||
uniPagesJSON: any = ""
|
||||
|
||||
constructor({ includes, pagesJsonDir }: any) {
|
||||
this.includes = includes
|
||||
this.uniPagesJSON = JSON.parse(stripJsonComments(fs.readFileSync(pagesJsonDir, "utf-8")))
|
||||
this.routes = this.getPagesRoutes().concat(this.getSubPackagesRoutes())
|
||||
this.tabbar = this.getTabbarRoutes()
|
||||
}
|
||||
|
||||
getPagesRoutes(pages = this.uniPagesJSON.pages, rootPath = null) {
|
||||
let routes = []
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const item = pages[i]
|
||||
let route: any = {}
|
||||
for (let j = 0; j < this.includes.length; j++) {
|
||||
const key = this.includes[j]
|
||||
let value = item[key]
|
||||
if (key === "path") {
|
||||
value = rootPath ? `/${rootPath}/${value}` : `/${value}`
|
||||
}
|
||||
if (key === "aliasPath" && i == 0 && rootPath == null) {
|
||||
route[key] = route[key] || "/"
|
||||
} else if (value !== undefined) {
|
||||
route[key] = value
|
||||
}
|
||||
}
|
||||
routes.push(route)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
getSubPackagesRoutes() {
|
||||
if (!(this.uniPagesJSON && this.uniPagesJSON.subPackages)) {
|
||||
return []
|
||||
}
|
||||
const subPackages = this.uniPagesJSON.subPackages
|
||||
let routes: any = []
|
||||
for (let i = 0; i < subPackages.length; i++) {
|
||||
const subPages = subPackages[i].pages
|
||||
const root = subPackages[i].root
|
||||
const subRoutes = this.getPagesRoutes(subPages, root)
|
||||
routes = routes.concat(subRoutes)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
getTabbarRoutes() {
|
||||
if (!(this.uniPagesJSON && this.uniPagesJSON.tabBar && this.uniPagesJSON.tabBar.list)) {
|
||||
return []
|
||||
}
|
||||
const tabbar = this.uniPagesJSON.tabBar.list
|
||||
let tabbarMap: any = []
|
||||
tabbar.forEach((bar: { pagePath: string }) => {
|
||||
tabbarMap.push("/" + bar.pagePath)
|
||||
})
|
||||
return tabbarMap
|
||||
}
|
||||
}
|
||||
|
||||
export const useRouter = ({ pagesJsonDir, includes = [] }: any) => {
|
||||
let defaultIncludes = ["path", "aliasPath", "name"]
|
||||
includes = [...defaultIncludes, ...includes]
|
||||
let pages = new TransformPages({
|
||||
pagesJsonDir,
|
||||
includes,
|
||||
})
|
||||
if (pages.tabbar && pages.tabbar.length) {
|
||||
console.table(pages.tabbar)
|
||||
}
|
||||
if (pages.routes && pages.routes.length) {
|
||||
console.table(pages.routes)
|
||||
}
|
||||
return {
|
||||
name: "use-router",
|
||||
config() {
|
||||
return {
|
||||
define: {
|
||||
__ROUTES__: JSON.stringify(pages.routes),
|
||||
__TABBAR__: JSON.stringify(pages.tabbar),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { throttle } from "radash"
|
||||
|
||||
function paramsToQuery(params: { [key: string]: string }) {
|
||||
let query = []
|
||||
for (let key in params) {
|
||||
// @ts-ignore
|
||||
query.push(key + "=" + params[key])
|
||||
}
|
||||
return query.join("&")
|
||||
}
|
||||
|
||||
// 路由跳转
|
||||
function _jump(path = "", params = {}, type = 1) {
|
||||
if (!path) {
|
||||
console.error("router::jump>>path 不能为空")
|
||||
}
|
||||
if (hasTabbar(path)) {
|
||||
uni.switchTab({ url: path })
|
||||
return
|
||||
}
|
||||
let url: string = ""
|
||||
if (Object.keys(params)) {
|
||||
url = `${path}?${paramsToQuery(params)}`
|
||||
}
|
||||
if (type == 1) {
|
||||
uni.navigateTo({ url: url })
|
||||
return
|
||||
}
|
||||
if (type == 2) {
|
||||
uni.redirectTo({ url: url })
|
||||
return
|
||||
}
|
||||
if (type == 3) {
|
||||
uni.reLaunch({ url: url })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 路由返回
|
||||
function _back(page = 1) {
|
||||
if (hasHistory()) {
|
||||
uni.navigateBack({
|
||||
delta: page,
|
||||
})
|
||||
} else {
|
||||
_jump(__TABBAR__[0], {})
|
||||
}
|
||||
}
|
||||
|
||||
export function jump(...args: any) {
|
||||
throttle({ interval: 500 }, () => _jump(...args))()
|
||||
}
|
||||
|
||||
export function back(...args: any) {
|
||||
throttle({ interval: 500 }, () => _back(...args))()
|
||||
}
|
||||
|
||||
export function hasHistory() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function getCurrentPage() {
|
||||
const pages = getCurrentPages()
|
||||
return pages[pages.length - 1]
|
||||
}
|
||||
|
||||
export function hasTabbar(str: string = "") {
|
||||
if (!str) {
|
||||
const { route } = getCurrentPage()
|
||||
if (route) {
|
||||
str = `/${route}`
|
||||
}
|
||||
}
|
||||
if (Array.isArray(__TABBAR__) && __TABBAR__.indexOf(str) !== -1) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
@font-face {
|
||||
font-family: "iconfont";
|
||||
/* Project id 4740692 */
|
||||
src: url('https://at.alicdn.com/t/c/font_4740692_aiftzos3ij.woff2?t=1771041067801') format('woff2'),
|
||||
url('https://at.alicdn.com/t/c/font_4740692_aiftzos3ij.woff?t=1771041067801') format('woff'),
|
||||
url('https://at.alicdn.com/t/c/font_4740692_aiftzos3ij.ttf?t=1771041067801') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-kefu:before {
|
||||
content: "\e677";
|
||||
}
|
||||
|
||||
.icon-kefu1:before {
|
||||
content: "\e612";
|
||||
}
|
||||
|
||||
.icon-info:before {
|
||||
content: "\e64a";
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e8d8";
|
||||
}
|
||||
|
||||
.icon-shezhi:before {
|
||||
content: "\e601";
|
||||
}
|
||||
|
||||
.icon-yiguoqi:before {
|
||||
content: "\e642";
|
||||
}
|
||||
|
||||
.icon-weishiyong:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
|
||||
.icon-weishiyong1:before {
|
||||
content: "\e623";
|
||||
}
|
||||
|
||||
.icon-icon-phone:before {
|
||||
content: "\e657";
|
||||
}
|
||||
|
||||
.icon-saoyisao:before {
|
||||
content: "\e658";
|
||||
}
|
||||
|
||||
.icon-wode:before {
|
||||
content: "\f0b4";
|
||||
}
|
||||
|
||||
.icon-a-kefu3x:before {
|
||||
content: "\e62e";
|
||||
}
|
||||
|
||||
.icon-fuzhi:before {
|
||||
content: "\e8b0";
|
||||
}
|
||||
|
||||
.icon-caipu:before {
|
||||
content: "\e600";
|
||||
}
|
||||
|
||||
.icon-erweima:before {
|
||||
content: "\e7c1";
|
||||
}
|
||||
|
||||
.icon-biaoqianlanicon_gouwuche-xuanzhong:before {
|
||||
content: "\e61c";
|
||||
}
|
||||
|
||||
.icon-a-:before {
|
||||
content: "\e651";
|
||||
}
|
||||
|
||||
.icon-a--:before {
|
||||
content: "\e652";
|
||||
}
|
||||
|
||||
.icon-dianhua:before {
|
||||
content: "\e614";
|
||||
}
|
||||
|
||||
.icon-yuding:before {
|
||||
content: "\e649";
|
||||
}
|
||||
|
||||
.icon-xiangxiajiantouxiao:before {
|
||||
content: "\e64e";
|
||||
}
|
||||
|
||||
.icon-youjiantou:before {
|
||||
content: "\e928";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@font-face {
|
||||
font-family: 'sheepicon';
|
||||
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAA7QAAsAAAAAH7gAAA6AAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACFGAquMKcBATYCJANYCy4ABCAFhGcHghMbGBszo8LGASAo3ovsvzzgDulfoYNbbIQFfeT6cUXKcnp8h6BAxr+OT0PaNfv10KZBODSIpU3sSo3EFhur478eSgk9tB9t3u5+1bOm2u/adRPIVAjdKiUTIiF5p0G7H4liOw9t80+OPDw84PjYX/CsAiMBxaj6kzAy38TIGQsWkbpo6xcu2kX11lw1dxwop50cColKPds3ntdm7TMQ5O5+/WLKMIHXtiAAiNIW12xIQlaYu4Gc3QMp9L00hYPmlmYUAIc59ZTtJbdsO9j2NSK25QLAc0DyxvKXihAq8ZKMoATv/f9zrzYZAHkEYdZa9V6Swn15H145H9pmnAJChmzHijgpJuV0xK6bIiH5+Bk9o+tkhfLTTSUJvfZo7JSOMoWx6YxH4gujkBDUd70OAql9x8x4+frt0KaW2NHmkBOFuKBmcLpD7kZznJkSu0PZHhMdAphlPpPwmn5/fbYxAiNokCc6ffJsOicffk0Me+0L7OXaXBW0bUAALNhes1htvv+VDWGbyikru3MPQOwrBkZTwKn1Plz08HMihm2VwHXeg0GnV6k1WqWCEwAi/AfPSqQyOTFQh+TKAVpqwGCvSnMxpv/A6ED/g9GDeqxChQlGDSowGlADRgtqwShBQzAK0AgMBxqDEYAmIMjRFATQDATQHASztAChCi1BAK1AAK1BAG3AEGgL8Zz0w7gU0MZTgOiRpBsy5RLFCuaCL5xPM0NODq8d2THAOMI4ubYfquKsiEWTK4eh6dLW43a8zOpckM+TjbnHdU1JVlrnxSTBlFHdSMq3EylMQYzHhjubO3aq7d5GobB13euVUhgAmWHC58lnTFrRDCqvztCU9YlrJOoRWxI1TTQmWeFHZ+NL4AbKEAGCSnvFWkQNs09QG4oOPwoNpWp3Igca3Ijt0enb5js0+TSgbcvz9Tps1wO/MfvfIdQN/VsKcnWwUYOTaKVfTMxPs3q66vjgClvbbTYEuoS6SS5OV0RrAdWvsMVLfXlLmcs1lWJhTSVP8rDHqbV5o6jTHIuLhmAA1flCHbspEhMFy864PMEQhCNNHWbhxhk258Iu6jgCf6sh0ILiPgCqts3YJqh/EKNO8AGsmKhmQdqr9at9GVEMbJSj53lt1snRPWqDSpyZxmMEVc9HzZ0oopAvsoXrKhGBfk3BDIABwH1B02U+xWzmM0Djl1mO4vw3BOHrXBr9hNHDMUQKzjQEqXwuDxZcWHRh2VGXJKqPeJO3/AZkVx9v27Q7tlvHnW6av6EgDxCdwsmuOGqBQEBJBve5cgEh+yxKeSj/iGUn9BMLq5EdXKYuW/OBjXp3vKOnKnHtPV8vhm+bYvfmscPd+A5r7efmxFbv1k97dX6Fwuo5nKA8Wg1unahNCDpxH5f4F1e1Avm+J3bkirM/B1AhHkJb4kpDSglrbCUS6Cqxa1VFyupCJ0dNYejan0vb1A8Ka/myLMlEUoiihO6nZuJk5AaJtJSPvodqG/gaybU0QfZtKrRS3O/bidlgV8Z3pKbI4cCo4yVvkkw7RTJ2UNK77Q9PQhPqxex+dB+QEx7whgM5Y3QV/RGOSYliqX7GSzpdq7OoBVTx2PrTMd1+Wd0fdauWJKmBIx995D0bDjNEGwnjvgBQpdML/A1fy1wmurOjsCdwpCei9v3dXPCw/mp6J3bRRPB9J/BPW50dhLadQYN6abeMHYYgl9xP6njrAKPFNiKR9DGX6VaI2bLFUmKKtfsOj5cmUYXWlagKhkFbOArjBptIBaP1RcxULU6yoVsDKN8lLIfQaRX9PIOvgxNsqg9RSccoe+H7LT3aJsOJLTGSgm4BWABvm1gL3igAG/BBHSyADIgUsk69ltYJBhd7rAa1Xrwt0eiCWKPL3U1zN6i0FvTdrbJ3m2qtoG5oQPdWD0c3QfV5WptcB/F2Srp2aHFbOoYcoXuPuXi3f1HUvfenYkQnUUiOVRNj8+RnXgePrgPANUIc4A4Hg0uvD2/GvuCAuhBH/xE3OqRefM8W68GISmsunTYDetfdZgWGjiCjXjBE6u5JiRBELt7MmrRUTYfm/KSFVmKVIk6ozxbI3d3lguxvgpzOJvfIEXzNGfMeXrXvhea5ZO7ZAtKZsKwE+3geP84hcdE+gR8fx090UCXaS3AUtUX7yDCWl2Avygo7dSpsetpwKhhWgO7PndKvF+9a5hyra1mZohfrXOYk1dWpaGZGnujv/usNLOx3nLOW3wpbC+hGcUd8RsgPEFesSP9ftz/f67st6ZdqBeh7ryItSaFS6Cb0HieoRRQZ6X7g4ZrLDbm5kYGmUXz1yOrUwo91/49qMisALeESc954nrnE2ma67UPDEjplso2V3Z9xJ2EJKLAxM80k0Vih2JxokvYv7944UaFIN04z+ZdhTGyZeixzl7i4SNxl74D64hIXmfu7nF1O4RidtL90C50cGO/ruzVOLjt0yMdfaVD8GJ0QyZFETU42avH0mk6RmTg5FEaePkXaPs5Lc2XdYvXOX6wGfcly7aZvvea2Zfl8ALePB1OVvjRmCPsN7421NaQzdRVUqBeiuhZke1mEdqkhOK4a2Av6KuvYS7wlmyXbd7E9AvQ2y4PbKRHXQ1QuBNHyn9wjcXRSdWBQ2su62Z+SBkECP+MgnuVUW7G4CkBV/HGsTxsbw7T7sLAdtqY2R2tci6M9RrO5Scexfq3SJK1+zFbZWlrcSZX5Ootarw3phjEYpl4sBq3Rt5C2El8Tt5JiiDAlui2OYWsgjI9RlLUwJSXMBq1h79WmD1G5+LRBd2cW0hn/oOO27jPw+NbNTciKF1vT7EpMw4zCLIvfjcTJQhFlclguEcGTGhmXzyCTr5Gx88Wh5zzKNeWacUwR6YAjie5N96eLueLlUHkDwv/qo3RCy3Yc+P/q96nzy6qNdz3K0CpE/MWoJ9qW8FtOppYZh+qGRkEEsFaUJZRnuuV4fBdqr0B+DwCsdeIIvTy3+x4Fdlxpab2yfYqI74MYyszySDnEFIc0x+pDcB6cw5EaZg4RxR3Og51H5XKnjiFk4/v5pmmm+MXTNRUKZzo7hR1Zh4w0VomyPQTZwrxGCL9To3HQ4zzLI0+Ue/ZXlV0rUM1LSeVVtfFqUpKreO3dyGQhGFWBU17W1XEpRbxao0Zq3urstlfJe9IaP3lyunrifTS+J9nU3ywwTmhRCFEEDd1oUjw62X6hVsDulmbA0G5Y0KBApcvYinqv9GoqzgC6UcCx+amf39Hg2akQRc+to3PO4usH+Psd+c07FtKDwmjF8CnlyKmiYo+hdXzfXRAconJWOY2PH1G5qIKD7dAEdoJAQn1qbqGHQkY1wUkVUmWprrMwudrU3KL4PJdSKg9JqjEil1N1Bwss9azyLdfpQsCJhxFIjEGhNJXAwhj1vREaEhiJSY4gUsQ9+u7vaydqL68WPnayDAkgsQauI2EscPrq8qXbxvPGLz9+mrec//r1VryXb52+RvxoXzMfExMfM9/NRu31Mbn1N/eLRvFei4T5+cJGIWVpEublCZuB0KANZSh+F4N77vuD5uhXxFHlmOkI4g1RtX+lLC3kNctDQ53ms8ZkjVSDSAC2F4aFHQnhz2jqm/3w1POwQwlk2sIWGs1Hd+Szv4htPWdQfmNzbeqq1uuBN3jYd59PrYsnu8bf/7NNTu6xHmf0zuoYHQoyVlQyOebh645NtqY2EyzyWEOzNs6cxffvfbvUllafm7131z1FrOQR3BJwD0lG9yf4012g8whSrhMIYRGMFtwNUizHmeDkY9mCjkQ3sxhOCUgixQBcZ5x0ZOtGa2AMP4L/UUfMcr8hu7PCpqPT3sau7BhfJduTHQkKixuENuQH2AQ8/rSSNMHW5dNA2Etm+jtogjx7niSDaTEl4F1ChpcMQpU4sZo0Zp0xVEohtKnErSuyKX9MFCF/ZSwwyF7xdIlYYrfMhkLsb46YYSP20Q2XnLyf2bZXBANC3sIUtA3afiuP5U6eIRoQchemIVsSXc4wezTH27ZxKyMTIdlC0wu5jqwZl+e2Pt4+9jAOmYFR9fUwQ2NGlzBdsoe7p7CEGzbIA5wY52ku+oydLzL3dEyD8UDWCRiILoYB19KhGT//u3DlvUeGtja3054Y3FMdtrhmfeHK5/9TziIy6igWMVcaTL67hukZXi94bHBt70iZWSFcnMOUAFzEfqijsXKWwLanNivfnRXVzu4dLjcth33mYMoM7ANjFYuOPlD6gAUHHoOBqFspk1um/AZdWE3dXt1Cr2qh1nxQarSzqNPCzj/agiljFi/nKw5WzkwfyNjpbUW69vVJjKn4E8YoUOWOBwB/AjwFJtCVwFaBLsp23BqW+T5wP2j/+7PvBn91U9LQbLsbbACIaoK4LUh/BSpA+Hd0W3B/5LsqTcP9I5n613r7t723oWOKzeQthy34HSxJi7+a0x47/I9/yTqIf8yEVd/5UfI/5KRmczL6To5oBqR+Eng7qfV1XVydtPSz2tXFi43IAljW2EVwGLu5OJA5hBBjjxantpz0nuk8A6RMgFXPsVhqbxYbpbdgMfYdOIz9sjgwmiDE2H+LU9cqBpZZjWsLToxa0KjioQJQtHYLvdJvdJdac9HOf+QUsF3t81024xUjsg6bdHUHEassU6NevCfDuibVMpUIknuR9rjdWpZNDtR0ToxalssbpXhsHICi1Zf0z/eN7lJrluT/FU6OYF+5spfbSfCvcpSqcU2Kp6s7EFuxag5spzfKS1fBepRKSsvWSoSRP+craO3R1tezsnLevKu5CGROu1pJ92PYZOUUKFSkWMkvum87V6FSlWrFK0GJ4iVIlJ0BmiDDQqcAA08NDhpinIFHqLI6RJwKLine truncated
|
||||
}
|
||||
|
||||
[class*='sicon-'] {
|
||||
font-family: 'sheepicon';
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sicon-back:before {
|
||||
content: '\e706';
|
||||
}
|
||||
|
||||
.sicon-home:before {
|
||||
content: '\e70c';
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@import "font/sheepicon";
|
||||
@import "font/iconfont";
|
||||
|
||||
view,
|
||||
text,
|
||||
scroll-view {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
view,
|
||||
text {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
navigator {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
page {
|
||||
// 解决ios滑动不流畅
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
//英文文本不换行
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: transparent;
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createPinia } from "pinia"
|
||||
const pinia = createPinia()
|
||||
export default pinia
|
||||
@@ -0,0 +1,78 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { orderApi } from "../api"
|
||||
|
||||
export const useOnlineCartStore = defineStore("onlineCartStore", {
|
||||
state: () => ({
|
||||
cart: JSON.parse(uni.getStorageSync("onlineShopCart") || "[]"),
|
||||
shopMerchantId: uni.getStorageSync("onlineShopMerchantId") || "",
|
||||
}),
|
||||
getters: {
|
||||
totalCount(state) {
|
||||
return state.cart.reduce((prev: number, cur: { productCount: number }) => {
|
||||
return prev + cur.productCount
|
||||
}, 0)
|
||||
},
|
||||
totPrice(state) {
|
||||
return state.cart
|
||||
.map((item: any) => Number(item.totalPrice))
|
||||
.reduce((prev: number, cur: number) => prev + cur, 0)
|
||||
.toFixed(2)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
saveShopMerchantId(shopMerchantId: string) {
|
||||
this.shopMerchantId = shopMerchantId
|
||||
uni.setStorageSync("onlineShopMerchantId", this.shopMerchantId)
|
||||
},
|
||||
saveShopCart(cartList: any) {
|
||||
this.cart = cartList
|
||||
uni.setStorageSync("onlineShopCart", JSON.stringify(this.cart))
|
||||
},
|
||||
addGoodsItemToCart(goodsItem: any) {
|
||||
const findResult = this.cart.find((item: any) => {
|
||||
return item.productId === goodsItem.productId
|
||||
})
|
||||
if (!findResult) {
|
||||
this.cart.unshift(goodsItem)
|
||||
} else {
|
||||
findResult.productCount++
|
||||
}
|
||||
uni.setStorageSync("onlineShopCart", JSON.stringify(this.cart))
|
||||
},
|
||||
async addProductCount(productId: string) {
|
||||
this.cart.forEach((item: any) => {
|
||||
if (item.productId === productId) {
|
||||
item.productCount++
|
||||
}
|
||||
})
|
||||
await this.updateShopCart()
|
||||
await this.getShopCart()
|
||||
},
|
||||
async removeProductCount(productId: string) {
|
||||
this.cart.forEach((item: any) => {
|
||||
if (item.productId === productId) {
|
||||
item.productCount--
|
||||
}
|
||||
})
|
||||
await this.updateShopCart()
|
||||
await this.getShopCart()
|
||||
},
|
||||
async getShopCart() {
|
||||
const { data } = await orderApi.getShopCar({ merchantId: this.shopMerchantId, hardType: "SUPERMARKET" })
|
||||
this.saveShopCart(data ?? [])
|
||||
},
|
||||
async updateShopCart() {
|
||||
const list = this.cart
|
||||
.filter((v) => v.productCount && v.productCount > 0)
|
||||
.map((v) => {
|
||||
return {
|
||||
productCount: v.productCount,
|
||||
productId: v.productId,
|
||||
}
|
||||
})
|
||||
const { code }: any = await orderApi.updateShopCar({ merchantId: this.shopMerchantId, list, hardType: "SUPERMARKET" })
|
||||
if (code == 0) return await this.getShopCart()
|
||||
uni.showToast({ title: "更新购物车失败", icon: "none" })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineStore } from "pinia"
|
||||
|
||||
type State = {
|
||||
scanPayData: {
|
||||
mchId: string
|
||||
mchName: string
|
||||
payPrice: number | string
|
||||
}
|
||||
sourceWalletData: any
|
||||
}
|
||||
|
||||
export const usePayStore = defineStore("pay", {
|
||||
state: (): State => {
|
||||
return {
|
||||
scanPayData: {
|
||||
mchId: "",
|
||||
mchName: "",
|
||||
payPrice: 0,
|
||||
},
|
||||
sourceWalletData: {},
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 支付
|
||||
setScanPayData(data: any) {
|
||||
this.scanPayData = data
|
||||
},
|
||||
// 转账
|
||||
setSourceWalletData(data: any) {
|
||||
this.sourceWalletData = data
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { orderApi } from "../api"
|
||||
|
||||
// 共富商城购物车
|
||||
export const useProsperityCartStore = defineStore("prosperityCartStore", {
|
||||
state: (): any => ({
|
||||
cartList: [],
|
||||
}),
|
||||
getters: {
|
||||
totalCount(state) {
|
||||
return state.cartList.reduce((prev: number, cur: { productCount: number }) => {
|
||||
return prev + cur.productCount
|
||||
}, 0)
|
||||
},
|
||||
totPrice(state) {
|
||||
return state.cartList
|
||||
.map((item: any) => Number(item.totalPrice))
|
||||
.reduce((prev: number, cur: number) => prev + cur, 0)
|
||||
.toFixed(2)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 购物车添加商品项
|
||||
addGoodsItemToCart(goodsItem: any) {
|
||||
const findResult = this.cartList.find((item: any) => {
|
||||
return item.productId === goodsItem.productId
|
||||
})
|
||||
if (!findResult) {
|
||||
this.cartList.unshift(goodsItem)
|
||||
} else {
|
||||
findResult.productCount++
|
||||
}
|
||||
},
|
||||
// 购物车商品项数量减一
|
||||
async removeProductCount(productId: string, merchantId: string) {
|
||||
this.cartList.forEach((item: any) => {
|
||||
if (item.productId === productId) {
|
||||
item.productCount--
|
||||
}
|
||||
})
|
||||
await this.updateShopCart(merchantId)
|
||||
await this.getShopCart(merchantId)
|
||||
},
|
||||
// 购物车商品项数量加一
|
||||
async addProductCount(productId: string, merchantId: string) {
|
||||
this.cartList.forEach((item: any) => {
|
||||
if (item.productId === productId) {
|
||||
item.productCount++
|
||||
}
|
||||
})
|
||||
await this.updateShopCart(merchantId)
|
||||
await this.getShopCart(merchantId)
|
||||
},
|
||||
// 获取购物车商品列表
|
||||
async getShopCart(merchantId: string) {
|
||||
const { data } = await orderApi.getShopCar({ merchantId, hardType: "FARMER" })
|
||||
this.cartList = data ?? []
|
||||
},
|
||||
// 更新购物车商品列表
|
||||
async updateShopCart(merchantId: string) {
|
||||
const list = this.cartList
|
||||
.filter((v) => v.productCount && v.productCount > 0)
|
||||
.map((v) => {
|
||||
return {
|
||||
productCount: v.productCount,
|
||||
productId: v.productId,
|
||||
}
|
||||
})
|
||||
const { code }: any = await orderApi.updateShopCar({ merchantId, list, hardType: "FARMER" })
|
||||
if (code == 0) return await this.getShopCart(merchantId)
|
||||
uni.showToast({ title: "更新购物车失败", icon: "none" })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { APPENV } from "../constant"
|
||||
|
||||
export const isDevEnv = (): boolean => process.env.NODE_ENV === "development"
|
||||
|
||||
export const getBaseApiUrl = () => {
|
||||
let baseApiUrl = ""
|
||||
if (isDevEnv()) {
|
||||
baseApiUrl = import.meta.env.VITE_APP_DEV_SERVICE
|
||||
} else {
|
||||
baseApiUrl = import.meta.env.VITE_APP_PROD_SERVICE
|
||||
}
|
||||
return baseApiUrl
|
||||
}
|
||||
|
||||
export type AppEnvValue = (typeof APPENV)[keyof typeof APPENV]
|
||||
|
||||
export const getAppInfo = async (): Promise<AppEnvValue> => {
|
||||
return APPENV.WECHAT
|
||||
}
|
||||
|
||||
export const getLaunchParam = () => {
|
||||
return "canteen"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isDevEnv } from "./app"
|
||||
|
||||
const normalizeBase = (base: string | undefined) => {
|
||||
if (!base) return ""
|
||||
return base.endsWith("/") ? base.slice(0, -1) : base
|
||||
}
|
||||
|
||||
const ensureLeadingSlash = (url: string) => {
|
||||
if (!url) return ""
|
||||
return url.startsWith("/") ? url : `/${url}`
|
||||
}
|
||||
|
||||
export const filePath = (path?: string | null) => {
|
||||
const rawPath = (path || "").trim()
|
||||
if (!rawPath) return ""
|
||||
if (/^https?:\/\//i.test(rawPath)) return rawPath
|
||||
const devBase = normalizeBase(import.meta.env.VITE_IMAGE_DEV_PATH || import.meta.env.VITE_APP_DEV_SERVICE)
|
||||
const prodBase = normalizeBase(import.meta.env.VITE_APP_PROD_IMAGE)
|
||||
const base = isDevEnv() ? devBase : prodBase
|
||||
|
||||
if (!base) return rawPath
|
||||
|
||||
return `${base}${ensureLeadingSlash(rawPath)}`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 显示价格把分转换到元
|
||||
* @param price
|
||||
* @returns
|
||||
*/
|
||||
export function displayPrice(price: any, suffix: string = "元", fixed = 2) {
|
||||
if (typeof price === "undefined") return "0.00" + suffix
|
||||
const parsedNumber = typeof price === "string" ? parseFloat(price) : price
|
||||
return (parsedNumber / 100.0).toFixed(fixed) + suffix
|
||||
}
|
||||
|
||||
/**
|
||||
* 把显示价格改为分
|
||||
* @param price
|
||||
* @returns
|
||||
*/
|
||||
export const storePrice = (num: number | string | undefined) => {
|
||||
if (typeof num === "undefined") return 0
|
||||
const parsedNumber = typeof num === "string" ? parseFloat(num) : num
|
||||
return Math.round(parsedNumber * 100)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import Request from "luch-request"
|
||||
import { isEmpty } from "radash"
|
||||
|
||||
import { router, constant } from "@@/index"
|
||||
import { getBaseApiUrl, getAppInfo, isDevEnv } from "@@/utils/app"
|
||||
|
||||
import { userApi } from "../api"
|
||||
import { APPENV } from "../constant"
|
||||
|
||||
const TOKEN_KEY = constant.storageConstant.TOKEN
|
||||
const USER_INFO_KEY = constant.storageConstant.USERINFO
|
||||
const SKIP_AUTH_URLS = [
|
||||
"/thirdparty/thirdPartyOAth2",
|
||||
"/sys/auth/refresh-token",
|
||||
"/auth/oauth2/token",
|
||||
"/admin/user/forgot-password",
|
||||
"/admin/stats/access/track",
|
||||
]
|
||||
const SKIP_REFRESH_TOKEN = ["/sys/auth/refresh-token", "/auth/token/check_token"]
|
||||
|
||||
let refreshTokenLock = true
|
||||
|
||||
// 创建https请求实例
|
||||
let baseUrl = ""
|
||||
// #ifdef WEB
|
||||
baseUrl = import.meta.env.VITE_API_URL
|
||||
// #endif
|
||||
// #ifndef WEB
|
||||
baseUrl = getBaseApiUrl()
|
||||
// #endif
|
||||
|
||||
const https = new Request({
|
||||
baseURL: baseUrl,
|
||||
timeout: 15000,
|
||||
method: "GET",
|
||||
header: {
|
||||
Accept: "*/*",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
},
|
||||
withCredentials: true,
|
||||
custom: {
|
||||
showSuccess: false, // 成功提醒 默认使用后端返回值
|
||||
successMsg: "", // 成功消息
|
||||
showError: true, // 显示失败消息 默认显示
|
||||
errorMsg: "", // 失败提醒 默认使用后端返回信息
|
||||
loadingMsg: "加载中", // loading提醒文字
|
||||
auth: false, // 需要授权才能请求 默认放开
|
||||
},
|
||||
})
|
||||
|
||||
https.interceptors.request.use(async (config) => {
|
||||
const tokenStorage = uni.getStorageSync(TOKEN_KEY)
|
||||
const config_header: any = config.header || {}
|
||||
let tokenValue = getTokenValue(tokenStorage)
|
||||
// 设置租户请求头
|
||||
config_header["TENANT-ID"] = "1"
|
||||
if (import.meta.env.VITE_IS_TOC === "true") config_header["CLIENT-TOC"] = "Y"
|
||||
// 判断是否需要设置认证头
|
||||
if (!SKIP_AUTH_URLS.some((url) => config.url?.startsWith(url))) {
|
||||
if (!isEmpty(tokenStorage)) {
|
||||
// token存在,检查是否过期
|
||||
if (isTokenExpires(tokenStorage) && !SKIP_REFRESH_TOKEN.some((url) => config.url?.startsWith(url)) && refreshTokenLock) {
|
||||
debugger
|
||||
// 过期,刷新token
|
||||
const newTokenStorage = await refreshToken()
|
||||
if (newTokenStorage) {
|
||||
const tokenValue = getTokenValue(newTokenStorage)
|
||||
if (tokenValue) config_header.Authorization = tokenValue
|
||||
}
|
||||
}
|
||||
}
|
||||
config_header["Authorization"] = tokenValue
|
||||
}
|
||||
return config
|
||||
}, Promise.reject)
|
||||
|
||||
// 响应拦截器
|
||||
https.interceptors.response.use(
|
||||
async (response) => {
|
||||
// 账号密码登录响应结果特殊处理
|
||||
if (response.config.url === "/auth/oauth2/token") {
|
||||
return Promise.resolve({
|
||||
code: 0,
|
||||
data: response.data,
|
||||
msg: null,
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(response.data)
|
||||
},
|
||||
async (error) => {
|
||||
// 处理 424 错误(token过期)
|
||||
if (error.statusCode === 424) {
|
||||
// 开发环境直接清除token并跳转登录
|
||||
uni.removeStorageSync(TOKEN_KEY)
|
||||
uni.removeStorageSync(USER_INFO_KEY)
|
||||
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" })
|
||||
router.jump("/pages/root/login")
|
||||
}
|
||||
// 显示错误信息
|
||||
else if (error?.data?.msg) {
|
||||
uni.showToast({ title: error.data.msg, icon: "none", mask: true })
|
||||
}
|
||||
return Promise.resolve(error.data)
|
||||
},
|
||||
)
|
||||
|
||||
// 从tokenStorage中读取tokenValue值
|
||||
const getTokenValue = (tokenStorage: any): string | null => {
|
||||
return tokenStorage?.access_token ? `Bearer ${tokenStorage.access_token}` : null
|
||||
}
|
||||
|
||||
// 检查tokenStorage中tokenValue是否过期
|
||||
const isTokenExpires = (tokenStorage: any): boolean => {
|
||||
const { access_token } = tokenStorage
|
||||
const expiresDate = new Date(access_token?.expiresAt).getTime()
|
||||
return new Date().getTime() >= expiresDate
|
||||
}
|
||||
|
||||
async function refreshToken(): Promise<any> {
|
||||
refreshTokenLock = false
|
||||
try {
|
||||
// 调用接口判断token是否过期
|
||||
const tokenStorage = uni.getStorageSync(TOKEN_KEY)
|
||||
const { data } = await userApi.refreshToken(tokenStorage.refresh_token)
|
||||
if (data) {
|
||||
tokenStorage.access_token = data.access_token
|
||||
tokenStorage.refresh_token = data.refresh_token
|
||||
tokenStorage.expires_in = data.expires_in
|
||||
uni.setStorageSync(TOKEN_KEY, tokenStorage)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("APP登录意外的错误error:", error)
|
||||
return false
|
||||
} finally {
|
||||
refreshTokenLock = true
|
||||
}
|
||||
}
|
||||
|
||||
// 请求函数
|
||||
const request = (config: any) => {
|
||||
return https.middleware(config)
|
||||
}
|
||||
|
||||
export default request
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 699 B |
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,13 @@
|
||||
## 3.2.0(2024-10-11)
|
||||
* [注意]本版本不向下兼容,可以直接通过市场导入或者在插件目录右键升级到项目中
|
||||
* [3.2.x文档链接](https://tmui.design/)
|
||||
* 1. 本版本不向下兼容!!!!!
|
||||
* 2. 不再兼容nvue平台,请谨慎升级
|
||||
* 3. 本版本不是之前的3.1.x版本的升级版本,无法平移过度到本版本
|
||||
* 4. 本版本为全新的tmui4.0x版本的小程序版本,风格,标准,细节与其一样,属性也大多相同,但内部是本质的不同.不可以4.0x混用
|
||||
* 5. 本版本以webview app + 小程序为准兼容,并且不提供任何原生插件,如果需要原生请使用tmui4.0x来支持我.
|
||||
* 6. 本版本一样是免费的,可以自由的使用,商用,任意修改不用担心版权问题.但请保留版权信息.但不得发布为公共的竞品版本,发现立即追究版权.
|
||||
* 7. 本版本更新迭代比较慢,力求后续兼容鸿蒙系统及所有小程序,因此会逐步减少组件数量,但会保证组件质量,保留最基本的组件库功能.
|
||||
* 8. 如果需要赞助或者捐赠,可以通过官网付款码赞助支持我的开发或者购买tmui4.0x以支持.
|
||||
* 发布第一个3.2.0版本为公测版本
|
||||
* 如果有遇到问题请提交bug至[Gitee](https://gitee.com/LYTB/tm-vuetify-for-vue3)
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<view v-if="showAlert" @click="onclickbar" class="tmAlert" :style="_styleMap.bgStyle">
|
||||
<!--
|
||||
@slot 左边图标插槽
|
||||
-->
|
||||
<slot name="left">
|
||||
<view class="tmAlertLeft">
|
||||
<tm-icon :size="iconSize" :color="(_styleMap.textStyle['color'] as string)"
|
||||
:dark-color="(_styleMap.textStyle['color'] as string)" :name="_iconName"></tm-icon>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="tmAlertContent">
|
||||
<text :style="_styleMap.textStyle">
|
||||
<slot></slot>
|
||||
</text>
|
||||
</view>
|
||||
<view @click="closeAlert" v-if="_showClose" class="tmAlertRight">
|
||||
<tm-icon :size="iconSize" :color="(_styleMap.textStyle['color'] as string)"
|
||||
:dark-color="(_styleMap.textStyle['color'] as string)" :name="closeIcon"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { computed, ref, onMounted } from "vue"
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber,arrayNumberValidByStyleBorderColor, linearValid ,getUnit } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
|
||||
/**
|
||||
* @displayName 警告
|
||||
* @exportName tm-alert
|
||||
* @category 展示组件
|
||||
* @description 样式丰富常用警告提醒
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmAlert' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type styleMapType = {
|
||||
textStyle : Record<string, string>,
|
||||
bgStyle : Record<string, string>
|
||||
}
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 关闭时触发
|
||||
*/
|
||||
"close",
|
||||
/**
|
||||
* 组件被点击时触发
|
||||
*/
|
||||
"click"
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 类型
|
||||
* warn:警告
|
||||
* success:成功
|
||||
* error:错误
|
||||
* info:信息
|
||||
* primary:正常主题
|
||||
*/
|
||||
status: {
|
||||
type: String,
|
||||
default: "primary"
|
||||
},
|
||||
/**
|
||||
* 警告图标,不填写取status默认图标
|
||||
* 填写以填写为准
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 警告图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: "28"
|
||||
},
|
||||
closeIcon:{
|
||||
type: String,
|
||||
default: "close-line"
|
||||
},
|
||||
/**
|
||||
* 显示还是隐藏关闭按钮
|
||||
*/
|
||||
showClose:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: "28"
|
||||
},
|
||||
/**
|
||||
* 主题色,如果不填写以status为准
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 文字颜色,如果不填写以status为准
|
||||
*/
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},/**
|
||||
* 暗黑主题颜色,如果不填写自动计算
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 暗黑文字颜色,如果不填写自动计算
|
||||
*/
|
||||
fontDarkColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 它是建立在你没有提供color时才有效。
|
||||
* 如果提供了color是以你color为背景最终色。
|
||||
* thin浅色模式,
|
||||
* normal标准背景色
|
||||
*/
|
||||
skin: {
|
||||
type: String,
|
||||
default: "thin"
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
* 数组数字时
|
||||
* [全部]
|
||||
* [顶左,顶右,底右,底左]
|
||||
* [顶左,底右]
|
||||
* [顶左,顶右,底右]
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"round": {
|
||||
type: [String,Number,Array<string|number>],
|
||||
default: 16
|
||||
},
|
||||
/**
|
||||
* 边线
|
||||
* 数组数字时
|
||||
* 数组数字时
|
||||
* [全部]
|
||||
* [左,上,右,下]
|
||||
* [左右,上下]
|
||||
* [左,上,右]
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"border": {
|
||||
type: [String,Number,Array<string|number>],
|
||||
default: () : string[] => []
|
||||
},
|
||||
/**
|
||||
* 边框颜色
|
||||
* 格式同border边线。
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"borderColor": {
|
||||
type: [String,Array<string>],
|
||||
default: () : string[] => []
|
||||
},
|
||||
/**
|
||||
* 如果不填写,自动计算
|
||||
*/
|
||||
darkBorderColor: {
|
||||
type: [String,Array<string>],
|
||||
default: () : string[] => []
|
||||
},
|
||||
/**
|
||||
* 边线类型,默认solid,可以为none
|
||||
*/
|
||||
"borderStyle": {
|
||||
type: String,
|
||||
default: 'solid'
|
||||
},
|
||||
/**
|
||||
* 间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"margin": {
|
||||
type:[String,Number,Array<string|number>],
|
||||
default: () : string[] => ['24', '0', '24', '24'] as string[]
|
||||
},
|
||||
/**
|
||||
* 内间隙[x]全部,[x,x]左右,上下,[x,x,x]左上右,[x,x,x,x]左上右下
|
||||
* 空数组时取全局值
|
||||
*/
|
||||
"padding": {
|
||||
type:[String,Number,Array<string|number>],
|
||||
default: () : string[] => ['28', '20'] as string[]
|
||||
},
|
||||
/**
|
||||
* 自定义高度,可以是数字,单位或者百分比,auto
|
||||
*/
|
||||
"height": {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
/**
|
||||
* 宽,单位合法即可数字,字符串带单位,百分比,auto
|
||||
*/
|
||||
"width": {
|
||||
type: String,
|
||||
default: "auto"
|
||||
},
|
||||
})
|
||||
|
||||
const showAlert = ref(true)
|
||||
const _color = computed(() : string => {
|
||||
|
||||
let tcolor = attrs.color;
|
||||
if (tcolor == '') {
|
||||
tcolor = attrs.status;
|
||||
}
|
||||
if (config.mode == 'dark' && attrs.darkColor != '') {
|
||||
tcolor = attrs.darkColor
|
||||
}
|
||||
let color = getDefaultColor(tcolor)
|
||||
|
||||
return color;
|
||||
})
|
||||
const _fontColor = computed(() : string => {
|
||||
let tcolor = attrs.fontColor;
|
||||
if (config.mode == 'dark' && attrs.fontDarkColor != '') {
|
||||
tcolor = attrs.fontDarkColor
|
||||
}
|
||||
return tcolor;
|
||||
})
|
||||
const _isDark = computed(() : boolean => config.mode == 'dark')
|
||||
const _showClose= computed(() : boolean => attrs.showClose)
|
||||
const _fontSize = computed(() : string => covetUniNumber(attrs.fontSize))
|
||||
const _margin = computed(() : string => arrayNumberValidByStyleMP(attrs.margin).join(' '))
|
||||
const _padding = computed(() : string => arrayNumberValidByStyleMP(attrs.padding).join(' '))
|
||||
const _round = computed(() : string => arrayNumberValidByStyleMP(attrs.round).join(' '))
|
||||
|
||||
const _border = computed(() : string => arrayNumberValidByStyleMP(attrs.border).join(' '))
|
||||
const _borderColor = computed(() : string => arrayNumberValidByStyleBorderColor(attrs.borderColor).join(' '))
|
||||
|
||||
const _iconName = computed(():string=>{
|
||||
const iconsmap = new Map<string, string>([
|
||||
['warn','alert-line'],
|
||||
['success','check-double-line'],
|
||||
['error','close-circle-line'],
|
||||
['info','information-line'],
|
||||
['primary','notification-line'],
|
||||
])
|
||||
let dicon = iconsmap.get(attrs.status)
|
||||
return attrs.icon==''?(dicon==null?'':dicon!):attrs.icon
|
||||
})
|
||||
|
||||
const _styleMap = computed(() : styleMapType => {
|
||||
let bgStylemap:any = {}
|
||||
let textStylemap:any = {}
|
||||
let colorObj = getDefaultColorObj(_color.value, _color.value)
|
||||
|
||||
if (attrs.skin == 'thin') {
|
||||
colorObj = getThinColorObj(_color.value, _color.value, _isDark.value)
|
||||
}
|
||||
let defaultObj = colorObj.default
|
||||
bgStylemap["backgroundColor"] = defaultObj.background
|
||||
bgStylemap["margin"] = _margin.value
|
||||
bgStylemap["padding"] = _padding.value
|
||||
bgStylemap["border-radius"] = _round.value
|
||||
bgStylemap["border-width"] = _border.value
|
||||
bgStylemap["height"] = covetUniNumber(attrs.height)
|
||||
bgStylemap["width"] = covetUniNumber(attrs.width)
|
||||
bgStylemap["border-style"] = attrs.borderStyle
|
||||
if(attrs.borderColor.length>0){
|
||||
bgStylemap["border-color"] = _borderColor.value
|
||||
}else{
|
||||
bgStylemap["border-color"] = getDefaultColor(_color.value)
|
||||
}
|
||||
|
||||
|
||||
if (_fontColor.value == '') {
|
||||
textStylemap["color"] = defaultObj.fontColor
|
||||
} else {
|
||||
textStylemap["color"] = _fontColor.value
|
||||
}
|
||||
textStylemap["font-size"] = _fontSize.value
|
||||
return {
|
||||
bgStyle: bgStylemap,
|
||||
textStyle: textStylemap
|
||||
} as styleMapType;
|
||||
})
|
||||
|
||||
const closeAlert = ()=>{
|
||||
emits('close')
|
||||
showAlert.value = false
|
||||
}
|
||||
const onclickbar = ()=>{
|
||||
emits('click')
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tmAlert {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tmAlertContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmAlertLeft {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.tmAlertLeft {
|
||||
flex-shrink: 0;
|
||||
padding-right: 12rpx;
|
||||
}
|
||||
|
||||
.tmAlertRight {
|
||||
flex-shrink: 0;
|
||||
padding-left: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue"
|
||||
import {
|
||||
arrayNumberValid,
|
||||
arrayNumberValidByStyleMP,
|
||||
covetUniNumber,
|
||||
arrayNumberValidByStyleBorderColor,
|
||||
linearValid,
|
||||
getUnit,
|
||||
} from "../../libs/tool"
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors"
|
||||
import { useTmConfig } from "../../libs/config"
|
||||
/**
|
||||
* @displayName 角标
|
||||
* @exportName tm-badge
|
||||
* @category 展示组件
|
||||
* @description 角标,位置丰富。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmBadge" })
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const attrs = defineProps({
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: "20",
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "error",
|
||||
},
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
maxCount: {
|
||||
type: [String, Number],
|
||||
default: 99,
|
||||
},
|
||||
/**
|
||||
* 如果是boolean就显示角标小红点
|
||||
* 如果是string显示文本标签
|
||||
* 如果是数字显示数字并启用maxCount
|
||||
*/
|
||||
label: {
|
||||
type: [String, Number, Boolean],
|
||||
default: "",
|
||||
},
|
||||
// "right" | "left" | "bottomLeft" | "bottomRight" | "top" | "bottom"
|
||||
position: {
|
||||
type: String,
|
||||
default: "right",
|
||||
},
|
||||
// number[]
|
||||
offset: {
|
||||
type: Array,
|
||||
default: (): number[] => [0, 0] as number[],
|
||||
validator(val: number[]): boolean {
|
||||
if (val.length == 0 || val.length == 2) return true
|
||||
console.error("x:必须是长度为2的数字数组参数或者空数组")
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const _offset = computed(() => attrs.offset)
|
||||
const _isDot = computed(() => {
|
||||
if (typeof attrs.label === "boolean") return attrs.label
|
||||
return false
|
||||
})
|
||||
|
||||
const _cStyles = computed(() => {
|
||||
let trs = ""
|
||||
if (attrs.position == "right") {
|
||||
trs = "translate(50%, -50%)"
|
||||
} else if (attrs.position == "left") {
|
||||
trs = "translate(-50%, -50%)"
|
||||
} else if (attrs.position == "bottomLeft") {
|
||||
trs = "translate(-50%, 50%)"
|
||||
} else if (attrs.position == "bottomRight") {
|
||||
trs = "translate(50%, 50%)"
|
||||
} else if (attrs.position == "top") {
|
||||
trs = "translate(0%, -50%)"
|
||||
} else if (attrs.position == "bottom") {
|
||||
trs = "translate(0%, 50%)"
|
||||
}
|
||||
|
||||
let top = ""
|
||||
let bottom = ""
|
||||
let left = ""
|
||||
let right = ""
|
||||
|
||||
if (attrs.position == "top") {
|
||||
top = "0px"
|
||||
left = "auto"
|
||||
right = "auto"
|
||||
} else if (attrs.position == "bottom") {
|
||||
bottom = "0px"
|
||||
left = "auto"
|
||||
right = "auto"
|
||||
} else if (attrs.position == "right") {
|
||||
top = _offset.value[1].toString() + "px"
|
||||
right = _offset.value[0].toString() + "px"
|
||||
} else if (attrs.position == "left") {
|
||||
top = "0px"
|
||||
left = "0px"
|
||||
} else if (attrs.position == "bottomLeft") {
|
||||
bottom = "0px"
|
||||
left = "0px"
|
||||
} else if (attrs.position == "bottomRight") {
|
||||
bottom = "0px"
|
||||
right = "0px"
|
||||
}
|
||||
|
||||
let dotMapCs: { [key: string]: any } = {}
|
||||
dotMapCs["background"] = getDefaultColor(attrs.bgColor)
|
||||
dotMapCs["left"] = left
|
||||
dotMapCs["right"] = right
|
||||
dotMapCs["top"] = top
|
||||
dotMapCs["bottom"] = bottom
|
||||
dotMapCs["transform"] = trs
|
||||
|
||||
let labelMapCs: { [key: string]: any } = {}
|
||||
labelMapCs["background"] = getDefaultColor(attrs.bgColor)
|
||||
labelMapCs["left"] = left
|
||||
labelMapCs["right"] = right
|
||||
labelMapCs["top"] = top
|
||||
labelMapCs["bottom"] = bottom
|
||||
labelMapCs["transform"] = trs
|
||||
labelMapCs["visibility"] = _label.value == "" ? "hidden" : "visible"
|
||||
|
||||
return [dotMapCs, labelMapCs]
|
||||
})
|
||||
|
||||
const _fontColor = computed(() => getDefaultColor(attrs.fontColor))
|
||||
const _fontSize = computed(() => covetUniNumber(attrs.fontSize, config.unit))
|
||||
const _label = computed((): string => {
|
||||
if (typeof attrs.label === "boolean") return ""
|
||||
if (typeof attrs.label === "string") return attrs.label
|
||||
let count = attrs.label
|
||||
let maxCount = typeof attrs.maxCount == "number" ? attrs.maxCount : parseInt(attrs.maxCount)
|
||||
if (count > 0 && count <= maxCount) return count.toString()
|
||||
if (count <= 0) return ""
|
||||
return maxCount.toString() + "+"
|
||||
})
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmBadge">
|
||||
<view class="tmBadgeWrap">
|
||||
<text :style="_cStyles[0]" class="tmBadge-dot" :class="[_isDot ? 'noneShow' : 'nonex']"></text>
|
||||
<view id="tmBadge-countAndLabel" class="tmBadge-countAndLabel" :class="[_isDot ? 'nonex' : 'noneShow']" :style="_cStyles[1]">
|
||||
<text class="tmBadge-countAndLabelText" :style="{ color: _fontColor, fontSize: _fontSize }">{{ _label }}</text>
|
||||
</view>
|
||||
<!--
|
||||
@slot 默认内容插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tmBadge {
|
||||
overflow: visible;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmBadge-countAndLabel {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
border-radius: 100px;
|
||||
min-width: 0px;
|
||||
padding: 0rpx 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tmBadge-countAndLabelText {
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tmBadge-dot {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 18px;
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.noneShow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nonex {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tmBadgeWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,583 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch, type PropType } from "vue"
|
||||
import { useTmConfig } from "../../libs/config"
|
||||
import { tmDate, type tmDateTypeTime, createDate } from "../../libs/tmDate"
|
||||
import { getDefaultColor } from "../../libs/colors"
|
||||
|
||||
type PICKER_ITEM_INFO = Record<string, any>
|
||||
|
||||
/**
|
||||
* @displayName 时间区间选择
|
||||
* @exportName tm-between-time
|
||||
* @category 表单组件
|
||||
* @description 快速的时间区间选择器,方便时间选择自动判断前后时间大小并校正。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmBetweenTime" })
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type coverValue = {
|
||||
value: string[]
|
||||
str: string
|
||||
}
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second"
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。
|
||||
*/
|
||||
modelValue: {
|
||||
type: [Array<string>],
|
||||
default: (): string[] => [] as string[],
|
||||
},
|
||||
/**
|
||||
* 当前时间经过format格式化后输出的值。
|
||||
* 此值不会处理输入,只输出显示。
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 当前打开的状态。
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
modelShow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 顶部标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "请选择时间",
|
||||
},
|
||||
/**
|
||||
* 开始时间,请提供正确的时间格式
|
||||
*/
|
||||
start: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 结束时间,请提供正确的时间格式
|
||||
*/
|
||||
end: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 精确到的级别,这里只是展示,具体的返回值还是完整的值。
|
||||
* year:年
|
||||
* month:年月
|
||||
* day:年月日
|
||||
* hour:年月日小时
|
||||
* minute:年月日小时分钟
|
||||
* second:年月日小时分钟秒
|
||||
*/
|
||||
type: {
|
||||
type: String as PropType<ModelType>,
|
||||
default: "day",
|
||||
},
|
||||
/**
|
||||
* 输出时间格式,只对v-model:modelStr及输入框展示有效
|
||||
* 因此它可能不是一个标准时间,比如YY SS ,所以不能作为modelValue使用
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD",
|
||||
},
|
||||
/**
|
||||
* 上方的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => ["年", "月", "日", "时", "分", "秒"] as string[],
|
||||
},
|
||||
/**
|
||||
* 快速时间区间选择,如果直接填写数字字符,会以你提供的数字最近多少来天来算。
|
||||
* d:本日
|
||||
* w:本周
|
||||
* m:本月
|
||||
* y:本年
|
||||
* q:本季度
|
||||
* 7:最近7天,后面的依此类推,数字的就是最近xx天。
|
||||
*/
|
||||
quickDate: {
|
||||
type: [Array<string>],
|
||||
default: (): string[] => ["d", "w", "m", "y", "q"] as string[],
|
||||
},
|
||||
/**
|
||||
* 是否懒加载内部内容。
|
||||
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
|
||||
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
|
||||
*/
|
||||
lazyContent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 如果你的快捷选择较多可能会让高度不足,需要自行设置下高。
|
||||
*/
|
||||
drawerSize: {
|
||||
type: String,
|
||||
default: "1020",
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
"cancel",
|
||||
/**
|
||||
* 确认触发
|
||||
* @param {string} date 当前选中时间
|
||||
*/
|
||||
"confirm",
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
"change",
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
"update:modelShow",
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
"update:modelStr",
|
||||
"update:modelValue",
|
||||
])
|
||||
|
||||
let startValue = new tmDate()
|
||||
let endValue = new tmDate()
|
||||
startValue.subtraction(1, "y")
|
||||
|
||||
const show = ref(false)
|
||||
const nowValue = ref<string[]>(["", ""])
|
||||
const nowModelValue = ref<string[]>(["", ""])
|
||||
const startDate = ref(startValue)
|
||||
const endDate = ref(endValue)
|
||||
const changeIndex = ref(0)
|
||||
const yanchiDuration = ref(false)
|
||||
const quicklist = ref<coverValue[]>([])
|
||||
|
||||
const _lazyContent = computed(() => props.lazyContent)
|
||||
|
||||
const _start_date = computed(() => (props.start === "" ? startDate.value : new tmDate(props.start)))
|
||||
const _end_date = computed(() => (props.end === "" ? endDate.value : new tmDate(props.end)))
|
||||
const _start_date_str = computed(() => _start_date.value.format())
|
||||
const _end_date_str = computed(() => _end_date.value.format())
|
||||
const _start_date_str_format = computed(() => (nowValue.value[0] === "" ? "开始时间" : new tmDate(nowValue.value[0]).format(props.format)))
|
||||
const _end_date_str_format = computed(() => (nowValue.value[1] === "" ? "结束时间" : new tmDate(nowValue.value[1]).format(props.format)))
|
||||
|
||||
const _backgroundColor = computed(() => (config.mode === "dark" ? getDefaultColor(config.inputDarkColor) : getDefaultColor("#f5f5f5")))
|
||||
const _borderColor = computed(() => (config.mode === "dark" ? getDefaultColor(config.borderDarkColor) : getDefaultColor("#eaeaea")))
|
||||
const _activeBorderColor = computed(() => getDefaultColor(config.color))
|
||||
const _placeStyle = computed(() => (config.mode === "dark" ? "color:#c7c7c7;" : "color:#838383;"))
|
||||
const _fontColor = computed(() => (config.mode === "dark" ? "#efefef" : "#333"))
|
||||
const _isDark = computed(() => config.mode === "dark")
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newvalue: string[]) => {
|
||||
let sortvalue = sorDateVaild(validTimeDate(newvalue))
|
||||
let newvaluestr = sortvalue.join("")
|
||||
if (newvaluestr === nowModelValue.value.join("")) return
|
||||
nowValue.value = sortvalue
|
||||
nowModelValue.value = sortvalue
|
||||
tongbuModelStr()
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelShow,
|
||||
(newValue: boolean) => {
|
||||
if (newValue === show.value) return
|
||||
show.value = newValue
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.quickDate,
|
||||
(newvalue: string[]) => {
|
||||
quicklist.value = getQuickDateType()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
yanchiDuration.value = _lazyContent.value ? false : true
|
||||
|
||||
let str = validTimeDate(props.modelValue)
|
||||
nowValue.value = str
|
||||
nowModelValue.value = str
|
||||
quicklist.value = getQuickDateType()
|
||||
tongbuModelStr()
|
||||
})
|
||||
|
||||
function clearDate() {
|
||||
let dst = ["", ""] as string[]
|
||||
nowValue.value = dst
|
||||
}
|
||||
|
||||
function tongbuModelStr() {
|
||||
let str = coverStrVal()
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit("update:modelStr", str === "~" ? "" : str)
|
||||
}
|
||||
|
||||
function dateChangeView(datestr: string) {
|
||||
let nowvalu = nowValue.value.slice(0)
|
||||
nowvalu[changeIndex.value] = datestr
|
||||
let strStart = nowvalu[0]
|
||||
let strEnd = nowvalu[1]
|
||||
let types = getTypes()
|
||||
if (strEnd !== "" && strStart !== "") {
|
||||
if (changeIndex.value === 0) {
|
||||
if (new tmDate(strStart).isBetweenOf(new tmDate(strEnd), ">", types)) {
|
||||
strEnd = strStart
|
||||
}
|
||||
} else if (changeIndex.value === 1) {
|
||||
if (new tmDate(strEnd).isBetweenOf(new tmDate(strStart), "<", types)) {
|
||||
strStart = strEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
|
||||
function tagsClick(item: coverValue) {
|
||||
nowValue.value = item.value
|
||||
}
|
||||
|
||||
function inputClick(index: number) {
|
||||
changeIndex.value = index
|
||||
|
||||
let nowvalu = nowValue.value.slice(0)
|
||||
let strStart = nowvalu[0]
|
||||
let strEnd = nowvalu[1]
|
||||
|
||||
if (changeIndex.value === 0) {
|
||||
if (strStart === "") {
|
||||
strStart = strEnd !== "" ? strEnd : endDate.value.format()
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
} else if (changeIndex.value === 1) {
|
||||
if (strEnd === "") {
|
||||
strEnd = strStart !== "" ? strStart : endDate.value.format()
|
||||
nowValue.value = [strStart, strEnd]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getQuickDateType(): coverValue[] {
|
||||
let typelist: coverValue[] = []
|
||||
if (props.quickDate.length === 0) return typelist
|
||||
let list = props.quickDate
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let item: string = list[i]
|
||||
if (!isNaN(parseInt(item))) {
|
||||
let dshi = parseInt(item)! as number
|
||||
let date = new tmDate()
|
||||
let start = date.format()
|
||||
date.subtraction(dshi, "d")
|
||||
let end = date.format()
|
||||
|
||||
typelist.push({ value: [end, start], str: "最近" + item + "天" } as coverValue)
|
||||
} else {
|
||||
let date = new tmDate()
|
||||
let startFDate = "YYYY/DD/MM 00:00:00"
|
||||
let endFDate = "YYYY/DD/MM 23:59:59"
|
||||
let start = date.format(startFDate)
|
||||
let end = date.format(endFDate)
|
||||
let desc = "本日"
|
||||
if (item === "w") {
|
||||
start = date.getDateStartOf("w").format(startFDate)
|
||||
end = date.getDateEndOf("w").format(endFDate)
|
||||
desc = "本周"
|
||||
} else if (item === "m") {
|
||||
start = date.getDateStartOf("m").format(startFDate)
|
||||
end = date.getDateEndOf("m").format(endFDate)
|
||||
desc = "本月"
|
||||
} else if (item === "y") {
|
||||
start = date.getDateStartOf("y").format(startFDate)
|
||||
end = date.getDateEndOf("y").format(endFDate)
|
||||
desc = "本年"
|
||||
} else if (item === "q") {
|
||||
let nowq = date.getQuarter("")
|
||||
let itemqatar = nowq[0]
|
||||
start = itemqatar.start
|
||||
end = itemqatar.end
|
||||
desc = "本季度"
|
||||
}
|
||||
|
||||
typelist.push({ value: [start, end], str: desc } as coverValue)
|
||||
}
|
||||
}
|
||||
return typelist
|
||||
}
|
||||
|
||||
// 获取精确单位
|
||||
function getTypes(): tmDateTypeTime {
|
||||
if (props.type === "year") {
|
||||
return "y" as tmDateTypeTime
|
||||
} else if (props.type === "month") {
|
||||
return "m" as tmDateTypeTime
|
||||
} else if (props.type === "day") {
|
||||
return "d" as tmDateTypeTime
|
||||
} else if (props.type === "hour") {
|
||||
return "h" as tmDateTypeTime
|
||||
} else if (props.type === "minute") {
|
||||
return "M" as tmDateTypeTime
|
||||
} else if (props.type === "second") {
|
||||
return "s" as tmDateTypeTime
|
||||
}
|
||||
return "s" as tmDateTypeTime
|
||||
}
|
||||
|
||||
//验证提供的值是否合法,并纠正为合法值。
|
||||
function validTimeDate(val: string[]): string[] {
|
||||
let str = ["", ""]
|
||||
if (val.length >= 1) {
|
||||
str[0] = val[0]!
|
||||
}
|
||||
if (val.length >= 2) {
|
||||
str[1] = val[1]!
|
||||
}
|
||||
|
||||
return sorDateVaild(str)
|
||||
}
|
||||
|
||||
function sorDateVaild(str: string[]): string[] {
|
||||
let types = getTypes()
|
||||
str.sort((a: string, b: string): number => {
|
||||
return new tmDate(a).getTime(types) - new tmDate(b).getTime(types)
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
function openShow() {
|
||||
show.value = true
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emit("update:modelShow", true)
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
/**
|
||||
* 变量控制打开状态
|
||||
* 等同v-model:model-show
|
||||
*/
|
||||
emit("update:modelShow", false)
|
||||
cancelResetDataCol()
|
||||
if (_lazyContent.value) {
|
||||
yanchiDuration.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
yanchiDuration.value = true
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emit("cancel")
|
||||
show.value = false
|
||||
cancelResetDataCol()
|
||||
}
|
||||
|
||||
function cancelResetDataCol() {
|
||||
nowValue.value = nowModelValue.value.slice(0)
|
||||
}
|
||||
|
||||
function coverStrVal(): string {
|
||||
let estrt = nowValue.value[0]
|
||||
let eend = nowValue.value[1]
|
||||
let start = estrt === "" ? "" : new tmDate(estrt).format(props.format)
|
||||
let ebd = eend === "" ? "" : new tmDate(eend).format(props.format)
|
||||
return start + "~" + ebd
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
let str = coverStrVal()
|
||||
let tmdate: string[] = []
|
||||
if (nowValue.value[0] !== "" && nowValue.value[1] !== "") {
|
||||
tmdate = nowValue.value.slice(0)
|
||||
nowModelValue.value = tmdate
|
||||
}
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
emit("update:modelValue", tmdate)
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit("update:modelStr", str === "~" ? "" : str)
|
||||
|
||||
emit("confirm", tmdate)
|
||||
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view @click="openShow">
|
||||
<!--
|
||||
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
<tm-drawer
|
||||
@open="onOpen"
|
||||
:widthCoverCenter="true"
|
||||
:disabledScroll="true"
|
||||
@close="onClose"
|
||||
:showFooter="true"
|
||||
v-model:show="show"
|
||||
:size="drawerSize"
|
||||
:showClose="false"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<view
|
||||
style="
|
||||
padding: 0 20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
"
|
||||
>
|
||||
<tm-text>{{ title }}</tm-text>
|
||||
<tm-text @click="clearDate" style="opacity: 0.5">清空</tm-text>
|
||||
</view>
|
||||
</template>
|
||||
<view v-if="yanchiDuration" class="tmPickerDateWrap">
|
||||
<view v-if="quicklist.length > 0" class="tmPickerDateWrapQuickTags">
|
||||
<tm-tag
|
||||
font-size="26"
|
||||
:font-color="_isDark ? 'white' : ''"
|
||||
@click="tagsClick(item)"
|
||||
skin="thin"
|
||||
size="m"
|
||||
style="margin-right: 10px; margin-bottom: 5px"
|
||||
v-for="(item, index) in quicklist"
|
||||
:key="index"
|
||||
>{{ item.str }}</tm-tag
|
||||
>
|
||||
</view>
|
||||
<view v-if="quicklist.length > 0" style="height: 8px"></view>
|
||||
<view class="tmPickerDateWrapQuickInput">
|
||||
<view @click="inputClick(0)" style="flex: 1; height: 40px">
|
||||
<input
|
||||
:placeholder-style="_placeStyle"
|
||||
:style="{
|
||||
color: changeIndex == 0 ? _activeBorderColor : _fontColor,
|
||||
border: `2px solid ${changeIndex == 0 ? _activeBorderColor : _borderColor}`,
|
||||
backgroundColor: _backgroundColor,
|
||||
}"
|
||||
class="tmPickerInput"
|
||||
:disabled="true"
|
||||
:value="_start_date_str_format"
|
||||
/>
|
||||
</view>
|
||||
<tm-text style="width: 64px; text-align: center">至</tm-text>
|
||||
<view @click="inputClick(1)" style="flex: 1; height: 40px">
|
||||
<input
|
||||
:placeholder-style="_placeStyle"
|
||||
:style="{
|
||||
color: changeIndex == 1 ? _activeBorderColor : _fontColor,
|
||||
border: `2px solid ${changeIndex == 1 ? _activeBorderColor : _borderColor}`,
|
||||
backgroundColor: _backgroundColor,
|
||||
}"
|
||||
class="tmPickerInput"
|
||||
:disabled="true"
|
||||
:value="_end_date_str_format"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<tm-date-view
|
||||
v-if="show"
|
||||
@change="dateChangeView"
|
||||
:format="format"
|
||||
:type="type"
|
||||
:cell-units="cellUnits"
|
||||
:start="_start_date_str"
|
||||
:end="_end_date_str"
|
||||
:model-value="nowValue[changeIndex]"
|
||||
></tm-date-view>
|
||||
</view>
|
||||
<tm-icon v-if="!yanchiDuration" size="42" spin name="loader-line"></tm-icon>
|
||||
|
||||
<template #footer>
|
||||
<view class="flex px-4">
|
||||
<tm-button @click="onCancel" :block="true" skin="thin">取消</tm-button>
|
||||
|
||||
<tm-button @click="onConfirm" :block="true">确认</tm-button>
|
||||
</view>
|
||||
</template>
|
||||
</tm-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmPickerDateText {
|
||||
text-align: left;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: linear;
|
||||
transition-property: transform, opacity;
|
||||
transform: translateY(100%) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tmPickerDateWrapQuickInputPlackeTips {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tmPickerInput {
|
||||
border-radius: 80rpx;
|
||||
height: 80rpx;
|
||||
|
||||
padding: 0 20rpx;
|
||||
font-size: 30rpx;
|
||||
flex: 1;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tmPickerDateWrapQuickInput {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tmPickerDateWrapQuickTags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,541 @@
|
||||
<template>
|
||||
<view class="tmButton" :class="[_attrs.loading || _attrs.disabled ? 'tmButtonLoading' : '']"
|
||||
@touchstart="touchStart" @touchcancel="touchEnd" @touchend="touchEnd" @mousedown="touchStart"
|
||||
@mouseup="touchEnd" @mouseleave="touchEnd" :style="[
|
||||
{
|
||||
minWidth: _width,
|
||||
width: _attrs.block ? '100%' : 'auto',
|
||||
height: _attrs.btnIcon ? _width : _height,
|
||||
fontSize: _fontSize,
|
||||
},
|
||||
buttonStyle,
|
||||
]">
|
||||
<tm-icon v-if="_attrs.loading" name="loader-line" spin :color="buttonStyle.color"></tm-icon>
|
||||
<!--
|
||||
@slot icon图标
|
||||
-->
|
||||
<slot name="icon">
|
||||
<tm-icon v-if="!_attrs.loading && _attrs.icon" :name="_attrs.icon" :size="_attrs.iconSize || _fontSize"
|
||||
:color="buttonStyle.color"></tm-icon>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot>
|
||||
<text v-if="!_attrs.btnIcon"></text>
|
||||
</slot>
|
||||
<button @getphonenumber="getphonenumber" @getuserinfo="getuserinfo" @error="error" @opensetting="opensetting"
|
||||
@launchapp="launchapp" @contact="contact" @chooseavatar="chooseavatar"
|
||||
@agreeprivacyauthorization="agreeprivacyauthorization" @addgroupapp="addgroupapp"
|
||||
@chooseaddress="chooseaddress" @chooseinvoicetitle="chooseinvoicetitle" @subscribe="subscribe"
|
||||
@login="login" @im="im" :appParameter="_attrs.appParameter" :lang="_attrs.lang"
|
||||
:sessionFrom="_attrs.sessionFrom" :sendMessageTitle="_attrs.sendMessageTitle"
|
||||
:sendMessagePath="_attrs.sendMessagePath" :sendMessageImg="_attrs.sendMessageImg"
|
||||
:showMessageCard="_attrs.showMessageCard" :groupId="_attrs.groupId" :guildId="_attrs.groupId"
|
||||
:publicId="_attrs.publicId" :dataImId="_attrs.dataImId" :dataImType="_attrs.dataImType"
|
||||
:dataGoodsId="_attrs.dataGoodsId" :dataOrderId="_attrs.dataOrderId" :dataBizLine="_attrs.dataBizLine"
|
||||
@click="onClick" :open-type="_attrs.openType" class="tmButtonBlock"></button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, type PropType, computed, ref, getCurrentInstance } from "vue";
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import TmForm from "../tm-form/tm-form.vue";
|
||||
|
||||
/**
|
||||
* @displayName 按钮
|
||||
* @exportName tm-button
|
||||
* @category 常用组件
|
||||
* @description 圆角,主题可通过配置统一设置或者动态全局设置,使设计风格统一并保持一致性。让你的风格独一无二。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmButton' });
|
||||
const { config } = useTmConfig()
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
* @property {object} evt 事件参数
|
||||
*/
|
||||
'click',
|
||||
'getphonenumber',
|
||||
'getuserinfo',
|
||||
'error',
|
||||
'opensetting',
|
||||
'launchapp',
|
||||
'contact',
|
||||
'chooseavatar',
|
||||
'agreeprivacyauthorization',
|
||||
'addgroupapp',
|
||||
'chooseaddress',
|
||||
'chooseinvoicetitle',
|
||||
'subscribe',
|
||||
'login',
|
||||
'im'
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 尺寸大小,可选值:s/m/n/g/xs
|
||||
*/
|
||||
size: {
|
||||
type: String as PropType<"s" | "g" | "xs" | "m" | "n">,
|
||||
default: "n"
|
||||
},
|
||||
/**
|
||||
* 样式
|
||||
* 可选值:thin/outlined/text/normal/dashed
|
||||
*/
|
||||
skin: {
|
||||
type: String as PropType<"thin" | "outlined" | "text" | "normal" | "dashed">,
|
||||
default: "normal"
|
||||
},
|
||||
/**
|
||||
* 遵循规则:
|
||||
* string或者[x]:全部边线
|
||||
* [x,x]左右边线,上下边线
|
||||
* [x,x,x] 左,上,右
|
||||
* [x,x,x,x] 左,上,右,下
|
||||
*/
|
||||
borderWidth: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* 圆角半径
|
||||
* 左上,右上,右下,左下
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number, Array<string | number>],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 50%圆角
|
||||
*/
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**宽度 */
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**高度 */
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**开放能力 */
|
||||
openType: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**是否占据一行 */
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**是否禁用 */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**图标名称 */
|
||||
icon: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**图标大小 */
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否开启仅图标按钮模式,宽和高相等
|
||||
*/
|
||||
btnIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**是否加载中 */
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 自定义主题颜色 */
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 自定义背景 */
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义暗黑背景 */
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义字体颜色 */
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**自定义暗黑字体颜色 */
|
||||
darkFontColorColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/** 渐变,前面的color,bgColor,darkBgColor失效 */
|
||||
linear: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
validator: (val: string[]) => {
|
||||
if (val.length == 0) return true;
|
||||
let pass = val.length == 3;
|
||||
if (!pass) {
|
||||
console.error("tmui提醒:linear 参数格式错误,请检查,必须长度为3");
|
||||
}
|
||||
return pass
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 数字或者字符串时自动计算投影,它会投影大小
|
||||
* 数组时(必须为4),第一个x,第二为y,第三个为大小 ,第四个为投影颜色
|
||||
* 空字符串时,取全局配置,如果不想投影设置为none即可.
|
||||
*/
|
||||
shadow: {
|
||||
type: [String, Number, Array<string>],
|
||||
default: '',
|
||||
validator: (val: string | number | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length != 4) {
|
||||
console.error("tmui提醒:shadow 第一个x,第二为y,第三个为大小 ,第四个为投影颜色");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
formType: {
|
||||
type: String as PropType<"submit" | "reset" | "form">,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 跳转的地址链接,出现时点击click会跳转以/pages开头。navigator模式跳转
|
||||
*/
|
||||
url: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
appParameter: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
default: "en"
|
||||
},
|
||||
sessionFrom: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
sendMessageTitle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
sendMessagePath: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
sendMessageImg: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
showMessageCard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
guildId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
publicId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataImId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataImType: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataGoodsId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataOrderId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
dataBizLine: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
})
|
||||
type PropsKeyType = keyof typeof attrs;
|
||||
|
||||
const covetButtonSize = (ops: PropsKeyType, s: TM.BUTTON_SIZE, size?: string | number) => {
|
||||
const widths = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '88'],
|
||||
['s', '96'],
|
||||
['m', '120'],
|
||||
['n', '220'],
|
||||
['g', '300']
|
||||
|
||||
])
|
||||
const heights = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '44'],
|
||||
['s', '60'],
|
||||
['m', '72'],
|
||||
['n', '90'],
|
||||
['g', '100']
|
||||
|
||||
])
|
||||
const fontSizes = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '22'],
|
||||
['s', '24'],
|
||||
['m', '28'],
|
||||
['n', '32'],
|
||||
['g', '34']
|
||||
])
|
||||
const rounds = new Map<TM.BUTTON_SIZE, string>([
|
||||
['xs', '8'],
|
||||
['s', '10'],
|
||||
['m', '12'],
|
||||
['n', '16'],
|
||||
['g', '20']
|
||||
])
|
||||
if (ops === 'width') {
|
||||
return covetUniNumber(widths.get(s)!)
|
||||
}
|
||||
if (ops === 'height') {
|
||||
return covetUniNumber(heights.get(s)!)
|
||||
}
|
||||
if (ops === 'round') {
|
||||
if (size && s == 'n') {
|
||||
return covetUniNumber(size!)
|
||||
}
|
||||
return covetUniNumber(rounds.get(s)!)
|
||||
}
|
||||
return covetUniNumber(fontSizes.get(s)!);
|
||||
}
|
||||
const _attrs = computed(() => attrs);
|
||||
const _width = computed(() => {
|
||||
if (attrs.block) {
|
||||
return 'auto'
|
||||
}
|
||||
return attrs.width === '' ? covetButtonSize('width', attrs.size) : covetUniNumber(attrs.width)
|
||||
})
|
||||
const _height = computed(() => attrs.height === '' ? covetButtonSize('height', attrs.size) : covetUniNumber(attrs.height))
|
||||
const _fontSize = computed(() => attrs.fontSize === '' ? covetButtonSize('fontSize', attrs.size) : covetUniNumber(attrs.fontSize))
|
||||
|
||||
const isHover = ref(false);
|
||||
|
||||
const touchStart = () => {
|
||||
isHover.value = true;
|
||||
}
|
||||
const touchEnd = () => {
|
||||
isHover.value = false;
|
||||
}
|
||||
const buttonStyle = computed(() => {
|
||||
let style = {
|
||||
borderColor: ``,
|
||||
borderWidth: ``,
|
||||
borderStyle: ``,
|
||||
background: ``,
|
||||
color: ``,
|
||||
borderRadius: ``,
|
||||
boxShadow: `none`
|
||||
}
|
||||
let isDark = config.mode == 'dark';
|
||||
let color = attrs.color || config.color;
|
||||
let round = attrs.round === '' ? covetButtonSize('round', attrs.size, config.buttonRadius) : attrs.round;
|
||||
let linear = linearValid(attrs.linear)
|
||||
let background = linear || (attrs.bgColor ? getDefaultColor(attrs.bgColor) : '')
|
||||
|
||||
let fontColor = attrs.fontColor ? getDefaultColor(attrs.fontColor) : ''
|
||||
let borderStyle = attrs.skin == 'dashed' ? 'dashed' : 'solid';
|
||||
|
||||
if (isDark) {
|
||||
if (attrs.darkBgColor) {
|
||||
background = getDefaultColor(attrs.darkBgColor)
|
||||
}
|
||||
if (attrs.darkFontColorColor) {
|
||||
fontColor = getDefaultColor(attrs.darkFontColorColor)
|
||||
}
|
||||
}
|
||||
|
||||
let obj = getDefaultColorObj(color, color)
|
||||
if (attrs.skin == 'text') {
|
||||
obj = getTextColorObj(color, color, isDark)
|
||||
|
||||
} else if (attrs.skin == 'thin') {
|
||||
obj = getThinColorObj(color, color, isDark)
|
||||
|
||||
} else if (attrs.skin == 'outlined' || attrs.skin == 'dashed') {
|
||||
obj = getOutlineColorObj(color, color)
|
||||
}
|
||||
|
||||
if (obj) {
|
||||
if (isHover.value && !attrs.disabled && !attrs.loading) {
|
||||
style.color = linear ? '#ffffff' : fontColor || obj.active.fontColor;
|
||||
style.background = background || obj.active.background;
|
||||
style.borderStyle = borderStyle;
|
||||
style.borderColor = background || obj.active.borderColor;
|
||||
style.borderWidth = linear ? '0px' : arrayNumberValidByStyleMP(attrs.borderWidth, '0px').join(" ")
|
||||
style.borderRadius = attrs.rounded ? '50%' : arrayNumberValid(round, '0px').join(" ")
|
||||
} else {
|
||||
style.color = linear ? '#ffffff' : fontColor || obj.default.fontColor;
|
||||
style.background = background || obj.default.background;
|
||||
style.borderStyle = borderStyle;
|
||||
style.borderColor = background || obj.default.borderColor;
|
||||
style.borderWidth = linear ? '0px' : arrayNumberValidByStyleMP(attrs.borderWidth, '0px').join(" ")
|
||||
style.borderRadius = attrs.rounded ? '50%' : arrayNumberValid(round, '0px').join(" ")
|
||||
let shadow = 'none'
|
||||
if (Array.isArray(attrs.shadow)) {
|
||||
shadow = attrs.shadow.join(' ')
|
||||
} else if (attrs.shadow !== '' && attrs.shadow != 'none') {
|
||||
shadow = `0 2px ${covetUniNumber(attrs.shadow)} ${obj.default.shadow}`
|
||||
} else if (attrs.shadow === '') {
|
||||
if (Array.isArray(config.buttonShadow)) {
|
||||
shadow = config.buttonShadow.join(' ')
|
||||
} else if (config.buttonShadow && config.buttonShadow != 'none') {
|
||||
shadow = `0 2px ${covetUniNumber(config.buttonShadow)} ${obj.default.shadow}`
|
||||
}
|
||||
}
|
||||
|
||||
style.boxShadow = shadow
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
const onClick = (evt: any) => {
|
||||
emits('click', evt);
|
||||
if (_attrs.value.disabled || _attrs.value.loading) return;
|
||||
if (_attrs.value.url) {
|
||||
uni.navigateTo({
|
||||
url: _attrs.value.url,
|
||||
fail(error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (attrs.formType == 'submit' || attrs.formType == 'reset') {
|
||||
let parent = findParent(proxy)
|
||||
if (parent != null) {
|
||||
let ele = parent! as InstanceType<typeof TmForm>
|
||||
if (attrs.formType == 'submit') {
|
||||
ele.submit()
|
||||
} else if (attrs.formType == 'reset') {
|
||||
ele.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const getphonenumber = (evt: any) => emits('getphonenumber', evt)
|
||||
const getuserinfo = (evt: any) => emits('getuserinfo', evt)
|
||||
const error = (evt: any) => emits('error', evt)
|
||||
const opensetting = (evt: any) => emits('opensetting', evt)
|
||||
const launchapp = (evt: any) => emits('launchapp', evt)
|
||||
const contact = (evt: any) => emits('contact', evt)
|
||||
const chooseavatar = (evt: any) => emits('chooseavatar', evt)
|
||||
const agreeprivacyauthorization = (evt: any) => emits('agreeprivacyauthorization', evt)
|
||||
const addgroupapp = (evt: any) => emits('addgroupapp', evt)
|
||||
const chooseaddress = (evt: any) => emits('chooseaddress', evt)
|
||||
const chooseinvoicetitle = (evt: any) => emits('chooseinvoicetitle', evt)
|
||||
const subscribe = (evt: any) => emits('subscribe', evt)
|
||||
const login = (evt: any) => emits('login', evt)
|
||||
const im = (evt: any) => emits('im', evt)
|
||||
|
||||
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmForm") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmForm") return parents;
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmButton {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
line-height: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&.tmButtonLoading {
|
||||
opacity: 0.5;
|
||||
|
||||
.tmButtonBlock,
|
||||
& {
|
||||
cursor: no-drop;
|
||||
}
|
||||
}
|
||||
|
||||
.tmButtonBlock {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
border: 0px solid transparent;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
|
||||
&::after {
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,512 @@
|
||||
<template>
|
||||
<!-- @vue-ignore -->
|
||||
<view
|
||||
@click="onclick"
|
||||
:hover-start-time="_isLinksHover ? 50 : 0"
|
||||
:hover-stay-time="_isLinksHover ? 100 : 0"
|
||||
:hover-class="_isLinksHover ? 'cellHover' : ''"
|
||||
class="tmCell"
|
||||
:class="[_allAttr.card == true ? 'cellCard' : 'cardInset']"
|
||||
:style="{
|
||||
backgroundColor: _color,
|
||||
borderRadius: _cardRadius,
|
||||
minHeight: _minHeight,
|
||||
borderBottom: _allAttr.bottom && !_allAttr.card && !bottomBorderInsert ? `1px solid ${_bottomBorderColor}` : 'none',
|
||||
padding: `0px ${_lrPadding}`,
|
||||
}"
|
||||
>
|
||||
<view v-if="_icon != ''" class="tmCellAvatar" :style="{ width: _leftSize, height: _leftSize, borderRadius: _avatarRound }">
|
||||
<!--
|
||||
@slot 头像图标
|
||||
@binding {string} icon 图标名称
|
||||
-->
|
||||
<slot name="avatar" :icon="_icon">
|
||||
<tm-icon :color="_allAttr.iconColor" :size="_iconSize" :name="_icon"></tm-icon>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="tmCellWrap"
|
||||
:style="{
|
||||
padding: `${_tbPadding} 0px `,
|
||||
borderBottom: _allAttr.bottom && !_allAttr.card && bottomBorderInsert ? `1px solid ${_bottomBorderColor}` : 'none',
|
||||
}"
|
||||
>
|
||||
<view class="center">
|
||||
<!--
|
||||
@slot 默认标题插槽
|
||||
-->
|
||||
<slot>
|
||||
<text class="title" :style="{ color: _titleColor, fontSize: _titleSize }">{{ _allAttr.title }}</text>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 简介
|
||||
@binding {string} desc 简介
|
||||
-->
|
||||
<slot name="desc" :desc="_allAttr.desc">
|
||||
<tm-text v-if="_allAttr.desc != ''" size="28" color="#bfbfbf" dark-color="#bfbfbf" class="desc">{{ _allAttr.desc }}</tm-text>
|
||||
</slot>
|
||||
</view>
|
||||
<view class="tmCellRight">
|
||||
<!--
|
||||
@slot 右边文字
|
||||
@binding {string} label 标签内容
|
||||
-->
|
||||
<slot name="label" :label="_allAttr.label">
|
||||
<text v-if="_allAttr.label != ''" :style="{ color: _allAttr.labelColor, fontSize: _rightLableSize }" class="rightLabel">{{
|
||||
_allAttr.label
|
||||
}}</text>
|
||||
</slot>
|
||||
<!--
|
||||
@slot 右插槽
|
||||
-->
|
||||
<slot name="right"></slot>
|
||||
<tm-icon
|
||||
v-if="_allAttr.url != '' || _allAttr.link"
|
||||
color="#bfbfbf"
|
||||
size="36"
|
||||
:name="attrs.linkIcon"
|
||||
_style="paddingLeft:12rpx"
|
||||
></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from "vue"
|
||||
import {
|
||||
arrayNumberValid,
|
||||
arrayNumberValidByStyleMP,
|
||||
covetUniNumber,
|
||||
arrayNumberValidByStyleBorderColor,
|
||||
linearValid,
|
||||
getUnit,
|
||||
} from "../../libs/tool"
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors"
|
||||
import { useTmConfig } from "../../libs/config"
|
||||
/**
|
||||
* @displayName 列表
|
||||
* @exportName tm-cell
|
||||
* @category 展示组件
|
||||
* @description 样式丰富常用警告提醒
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmCell" })
|
||||
const { config } = useTmConfig()
|
||||
type tmCellItemType = {
|
||||
icon: string
|
||||
title: string
|
||||
desc: string
|
||||
label: string
|
||||
bottom: boolean
|
||||
link: boolean
|
||||
url: string
|
||||
iconColor: string
|
||||
labelColor: string
|
||||
card: boolean
|
||||
}
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 项目点击
|
||||
*/
|
||||
"click",
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 左图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 左侧图标、头像圆角。默认为8
|
||||
*/
|
||||
avatarRound: {
|
||||
type: String,
|
||||
default: "8",
|
||||
},
|
||||
/**
|
||||
* 背景的主题色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
/**
|
||||
* 暗黑背景的主题色,空值时取sheetDarkColor
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 图标色,空值时取全局主题值。
|
||||
*/
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题",
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: "black",
|
||||
},
|
||||
/**
|
||||
* 暗黑标题颜色,如果不填写取白
|
||||
*/
|
||||
darkTitleColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
/**
|
||||
* 标题大小
|
||||
*/
|
||||
titleSize: {
|
||||
type: [String, Number],
|
||||
default: "32",
|
||||
},
|
||||
/**
|
||||
* 左边图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "42",
|
||||
},
|
||||
/**
|
||||
* 右边文本
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 右边文本颜色
|
||||
*/
|
||||
labelColor: {
|
||||
type: String,
|
||||
default: "#bfbfbf",
|
||||
},
|
||||
/**
|
||||
* 右侧label文字大小
|
||||
*/
|
||||
labelSize: {
|
||||
type: [String, Number],
|
||||
default: "24",
|
||||
},
|
||||
/**
|
||||
* 标题正文的简介文本
|
||||
*/
|
||||
desc: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 是否显示下边线
|
||||
*/
|
||||
showBottomBorder: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 是否让下边线显示居右,不贯穿到左边。
|
||||
*/
|
||||
bottomBorderInsert: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 下边线的颜色。如果你设定了的话。
|
||||
* 暗黑的边颜色失效,采用你自定的颜色。
|
||||
*/
|
||||
bottomBorderColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 是否显示链接状态,有点按效果。包括出现右边跳转指示。
|
||||
* 关闭的话,事件反应和跳转会更快。
|
||||
* 如果true右侧箭头图标会显示
|
||||
*/
|
||||
link: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 需要跳转的页面地址。
|
||||
* 如果填写了右侧箭头图标会显示
|
||||
*/
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
openType: {
|
||||
type: String,
|
||||
default: "navigate",
|
||||
},
|
||||
/**
|
||||
* 是否是卡片模式
|
||||
*/
|
||||
card: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 卡片模式圆角,不填写采用全局的cardRadius属性值.
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 左边图标区域宽和高的大小。
|
||||
*/
|
||||
leftSize: {
|
||||
type: [String, Number],
|
||||
default: "60",
|
||||
},
|
||||
/**
|
||||
* 最小高度,主要是用来统一风格高度不至于让点击范围过小
|
||||
* 如果你需要紧凑型可以设置为auto
|
||||
*/
|
||||
minHeight: {
|
||||
type: [String, Number],
|
||||
default: "88",
|
||||
},
|
||||
/**
|
||||
* 左右的间隙。
|
||||
*/
|
||||
lrPadding: {
|
||||
type: [String, Number],
|
||||
default: "32",
|
||||
},
|
||||
linkIcon: {
|
||||
type: String,
|
||||
default: "arrow-right-s-line",
|
||||
},
|
||||
/**
|
||||
* 上下间隙
|
||||
*/
|
||||
tbPadding: {
|
||||
type: [String, Number],
|
||||
default: "32",
|
||||
},
|
||||
})
|
||||
|
||||
const _color = computed(() => {
|
||||
if (config.mode === "dark") {
|
||||
if (attrs.darkColor !== "") return getDefaultColor(attrs.darkColor)
|
||||
return getDefaultColor(config.sheetDarkColor)
|
||||
}
|
||||
return getDefaultColor(attrs.color)
|
||||
})
|
||||
|
||||
const _titleColor = computed(() => {
|
||||
if (config.mode === "dark") {
|
||||
if (attrs.darkTitleColor !== "") return getDefaultColor(attrs.darkTitleColor)
|
||||
return "#ffffff"
|
||||
}
|
||||
return getDefaultColor(attrs.titleColor)
|
||||
})
|
||||
|
||||
const _leftSize = computed(() => covetUniNumber(attrs.leftSize))
|
||||
const _lrPadding = computed(() => covetUniNumber(attrs.lrPadding))
|
||||
const _tbPadding = computed(() => covetUniNumber(attrs.tbPadding))
|
||||
|
||||
const _avatarRound = computed(() => covetUniNumber(attrs.avatarRound))
|
||||
|
||||
const _minHeight = computed(() => covetUniNumber(attrs.minHeight))
|
||||
|
||||
const _bottomBorderColor = computed(() => {
|
||||
if (attrs.bottomBorderColor !== "") return getDefaultColor(attrs.bottomBorderColor)
|
||||
if (config.mode === "dark") return config.borderDarkColor
|
||||
return "#f5f5f5"
|
||||
})
|
||||
|
||||
const icon = ref("")
|
||||
|
||||
const _allAttr = computed(() => {
|
||||
let iconColor = attrs.iconColor
|
||||
if (iconColor === "") {
|
||||
iconColor = config.color
|
||||
}
|
||||
let p = {
|
||||
icon: icon.value,
|
||||
title: attrs.title,
|
||||
desc: attrs.desc,
|
||||
label: attrs.label,
|
||||
bottom: attrs.showBottomBorder,
|
||||
link: attrs.link,
|
||||
url: attrs.url,
|
||||
iconColor: getDefaultColor(iconColor),
|
||||
labelColor: getDefaultColor(attrs.labelColor),
|
||||
card: attrs.card,
|
||||
} as tmCellItemType
|
||||
return p
|
||||
})
|
||||
|
||||
const _cardRadius = computed(() => {
|
||||
if (attrs.round === "") return arrayNumberValidByStyleMP(config.cellRadius)
|
||||
return arrayNumberValidByStyleMP(attrs.round, config.unit)
|
||||
})
|
||||
|
||||
const _titleSize = computed(() => {
|
||||
let fontSize = covetUniNumber(attrs.titleSize, config.unit)
|
||||
if (config.fontSizeScale === 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _iconSize = computed(() => {
|
||||
let fontSize = covetUniNumber(attrs.iconSize, config.unit)
|
||||
if (config.fontSizeScale === 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 17
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _rightLableSize = computed(() => {
|
||||
let fontSize = covetUniNumber(attrs.labelSize, config.unit)
|
||||
|
||||
if (config.fontSizeScale === 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 13
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
|
||||
const _isLinksHover = computed(() => attrs.link)
|
||||
const _icon = computed(() => attrs.icon)
|
||||
|
||||
const onclick = () => {
|
||||
/**
|
||||
* 整个列表被点击
|
||||
*/
|
||||
emits("click")
|
||||
if (attrs.url != "") {
|
||||
if (attrs.openType == "navigate") {
|
||||
uni.navigateTo({
|
||||
url: attrs.url,
|
||||
})
|
||||
} else if (attrs.openType == "navigateBack") {
|
||||
uni.navigateBack({})
|
||||
} else if (attrs.openType == "reLaunch") {
|
||||
uni.reLaunch({
|
||||
url: attrs.url,
|
||||
})
|
||||
} else if (attrs.openType == "redirect") {
|
||||
uni.redirectTo({
|
||||
url: attrs.url,
|
||||
})
|
||||
} else if (attrs.openType == "switchTab") {
|
||||
uni.switchTab({
|
||||
url: attrs.url,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.cellHover {
|
||||
filter: contrast(90%);
|
||||
}
|
||||
|
||||
.tmCell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cardInset {
|
||||
// padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.cellCard {
|
||||
// padding: 0 32rpx;
|
||||
margin-bottom: 12rpx;
|
||||
margin-left: 24rpx;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.tmCellWrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
// padding: 32rpx 0rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
padding-top: 4rpx;
|
||||
}
|
||||
|
||||
.tmCellAvatar {
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.center {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmCellRight {
|
||||
padding-left: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rightLabel {
|
||||
line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 24rpx;
|
||||
text-align: right;
|
||||
max-width: 200rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, provide } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import tmCheckbox from '../tm-checkbox/tm-checkbox.vue';
|
||||
|
||||
/**
|
||||
* @displayName 多选框组
|
||||
* @exportName tm-checkbox-group
|
||||
* @page tm-checkbox
|
||||
* @category 表单组件
|
||||
* @description 可单单独使用,也可多选组合使用
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCheckboxGroup' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Array<string | number | boolean>],
|
||||
default: () => [] as string[],
|
||||
},
|
||||
direction: {
|
||||
type: String as PropType<"row" | "column">,
|
||||
default: "row",
|
||||
},
|
||||
align: {
|
||||
type: String as PropType<"flex-start" | "center" | "flex-end">,
|
||||
default: "left"
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
/**
|
||||
* 排列时之间的间隙,如果是数组第一项是列间隙,第二项是行间隙
|
||||
*/
|
||||
gap: {
|
||||
type: [Number, String, Array<string | number>],
|
||||
default: 20,
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'update:modelValue']);
|
||||
|
||||
const checkvaluelist = ref<Array<string | number | boolean>>([]);
|
||||
const isDestroy = ref(false);
|
||||
const id = "tmCheckboxGroup-" + getUid();
|
||||
|
||||
const _max = computed(() => props.max);
|
||||
const _gap = computed(() => arrayNumber(props.gap).join(" "))
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
checkvaluelist.value = newValue;
|
||||
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
checkvaluelist.value = props.modelValue;
|
||||
isDestroy.value = false;
|
||||
});
|
||||
|
||||
function addItem(item: string | number | boolean, ischange: boolean) {
|
||||
let index = checkvaluelist.value.findIndex((el) => el === item);
|
||||
if (index == -1) {
|
||||
if (checkvaluelist.value.length >= _max.value && _max.value > -1 && ischange && checkvaluelist.value.length > 0) {
|
||||
// uni.showToast({ title: "已是最大选择数量", icon: 'none', mask: true });
|
||||
checkvaluelist.value.splice(checkvaluelist.value.length - 1, 1);
|
||||
}
|
||||
checkvaluelist.value.push(item)
|
||||
} else {
|
||||
checkvaluelist.value.splice(index, 1);
|
||||
}
|
||||
|
||||
if (ischange) {
|
||||
emits("update:modelValue", checkvaluelist.value.slice(0));
|
||||
emits("change", checkvaluelist.value.slice(0));
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(item: string | number | boolean) {
|
||||
let index = checkvaluelist.value.findIndex((el) => el === item);
|
||||
if (index > -1) {
|
||||
checkvaluelist.value.splice(index, 1);
|
||||
emits("update:modelValue", checkvaluelist.value.slice(0));
|
||||
emits("change", checkvaluelist.value.slice(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
provide("tmCheckboxGroupValue", computed(() => checkvaluelist.value.slice(0)))
|
||||
|
||||
defineExpose({ addItem, removeItem })
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmCheckboxGroup"
|
||||
:style="{ 'flex-direction': direction, 'align-items': direction == 'row' ? 'flex-start' : undefined, 'justify-content': align, gap: _gap }">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.tmCheckboxGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,377 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, VueElement, inject } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
import tmCheckboxGroup from '../tm-checkbox-group/tm-checkbox-group.vue';
|
||||
import tmCheckbox from './tm-checkbox.vue';
|
||||
const proxy = getCurrentInstance()?.proxy as InstanceType<typeof tmCheckbox> | null;
|
||||
|
||||
/**
|
||||
* @displayName 多选框
|
||||
* @exportName tm-checkbox
|
||||
* @category 表单组件
|
||||
* @description 可单单独使用,也可多选组合使用
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCheckbox' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param {Boolean} check - 当前是否选中
|
||||
* @param {Number} value - 当前选中的值
|
||||
*/
|
||||
'change',
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前主题色,空值时取全局
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
|
||||
/**
|
||||
* 当前未选中时主题色,空值时取全局
|
||||
*/
|
||||
unCheckColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前未选中时的暗黑主题色
|
||||
*/
|
||||
darkUnCheckColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前双向绑定值
|
||||
*/
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 选中的值,
|
||||
*/
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 未选中的值
|
||||
*/
|
||||
unCheckValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 选中的图标名称。
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "check-line"
|
||||
},
|
||||
/**
|
||||
* 右侧文字。
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否隐藏选中框。然后利用默认插槽自定义选中所有样式和状态。
|
||||
*/
|
||||
hiddenCheckbox: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 半选中
|
||||
*/
|
||||
indeterminate: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 尺寸
|
||||
*/
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: "42"
|
||||
},
|
||||
/**
|
||||
* 中间小图标大小
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "40"
|
||||
},
|
||||
/**
|
||||
* 文字大小
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 如果不要显示label时设置为false可以隐藏label并把label间隙删除,
|
||||
*/
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const nowValue = ref<string | number | boolean>('');
|
||||
const boxId = ref("tmCheckbox-" + getUid());
|
||||
const tid = ref(0);
|
||||
const isDestroy = ref(false);
|
||||
const parentVal = inject("tmCheckboxGroupValue", computed((): Array<string | number | boolean> => []))
|
||||
const _color = computed(() => {
|
||||
if (props.color == "") return getDefaultColor(config.color);
|
||||
return getDefaultColor(props.color);
|
||||
});
|
||||
const _showLabel = computed(() => props.showLabel)
|
||||
|
||||
const _unCheckColor = computed(() => {
|
||||
if (config.mode == 'dark' && props.darkUnCheckColor != '') {
|
||||
return getDefaultColor(props.darkUnCheckColor);
|
||||
}
|
||||
if (props.unCheckColor == "") return getDefaultColor(config.unRadioAndCheckBoxColor);
|
||||
return getDefaultColor(props.unCheckColor);
|
||||
});
|
||||
|
||||
const _isCheck = computed(() => {
|
||||
return nowValue.value === props.value;
|
||||
});
|
||||
|
||||
const _disabled = computed(() => {
|
||||
return props.disabled;
|
||||
});
|
||||
|
||||
const _label = computed(() => {
|
||||
return props.label;
|
||||
});
|
||||
|
||||
const _indeterminate = computed(() => {
|
||||
return props.indeterminate;
|
||||
});
|
||||
|
||||
const _size = computed(() => {
|
||||
let size = covetUniNumber(props.size, config.unit);
|
||||
if (config.fontSizeScale == 1) return size;
|
||||
let sizeNumber = parseInt(size);
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 24;
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(size);
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue === nowValue.value) return;
|
||||
nowValue.value = newValue;
|
||||
// this.pushDataToParent();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroy.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
let t = this;
|
||||
isDestroy.value = false;
|
||||
nextTick(() => {
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) {
|
||||
nowValue.value = props.modelValue;
|
||||
}
|
||||
// if (nowValue.value !== props.unCheckValue) {
|
||||
// pushDataToParent(false);
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
function boxClick() {
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
emit('click');
|
||||
if (_disabled.value) return;
|
||||
/**
|
||||
* 要点提示:当本组件属性半选中状态时,应该继续保持选中状态,
|
||||
* 并触发change。
|
||||
*/
|
||||
if (_isCheck.value && !_indeterminate.value) {
|
||||
nowValue.value = props.unCheckValue;
|
||||
} else {
|
||||
nowValue.value = props.value;
|
||||
}
|
||||
/**
|
||||
* 当前选中的值,等同v-model
|
||||
*/
|
||||
emit('update:modelValue', nowValue.value);
|
||||
/**
|
||||
* 用户交互切换,选中变换时触发。
|
||||
* @param check {boolean} 当前是否选中
|
||||
* @param value {number} 当前选中的值
|
||||
*/
|
||||
emit('change', _isCheck.value, nowValue.value);
|
||||
|
||||
if (_isCheck.value) {
|
||||
pushDataToParent(true);
|
||||
|
||||
} else {
|
||||
removeValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setSelected(val: Array<string | number | boolean>) {
|
||||
if (!Array.isArray(val)) {
|
||||
throw new Error("val must be an array");
|
||||
}
|
||||
const isChecked = val.includes(props.value);
|
||||
if (isChecked) {
|
||||
nowValue.value = props.value;
|
||||
} else {
|
||||
nowValue.value = props.unCheckValue;
|
||||
}
|
||||
}
|
||||
|
||||
function pushDataToParent(isChange: boolean) {
|
||||
let pelement = findParent(proxy!);
|
||||
if (pelement == null) return;
|
||||
let parent: InstanceType<typeof tmCheckboxGroup> = pelement!;
|
||||
if (typeof parent?.addItem != 'function') return;
|
||||
parent.addItem(nowValue.value, isChange);
|
||||
}
|
||||
function removeValue() {
|
||||
let pelement = findParent(proxy!);
|
||||
if (pelement == null) return;
|
||||
let parent: InstanceType<typeof tmCheckboxGroup> = pelement!;
|
||||
if (typeof parent?.removeItem != 'function') return;
|
||||
parent.removeItem(props.value);
|
||||
}
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmCheckboxGroup") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmCheckboxGroup") return parents;
|
||||
return null;
|
||||
}
|
||||
|
||||
watch(parentVal, (newvalue: Array<string | number | boolean>) => {
|
||||
setSelected(newvalue)
|
||||
}, { deep: true })
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view :class="[_disabled ? 'checkboxDisabled' : '']" class="checkbox" @click="boxClick">
|
||||
<view class="checkboxBox" v-if="!hiddenCheckbox" :style="{
|
||||
backgroundColor: _isCheck ? _color : 'transparent',
|
||||
border: `1px solid ${_isCheck ? _color : _unCheckColor}`,
|
||||
width: _size,
|
||||
height: _size
|
||||
}">
|
||||
<view :class="['checkboxBoxIcon', 'checkboxBoxIcon_' + (nowValue ? 'on' : 'off')]">
|
||||
<tm-icon color="white" :name="_indeterminate ? 'subtract-line' : icon" :size="iconSize"></tm-icon>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view v-if="_showLabel" class="checkboxLabelBox" :class="[!hiddenCheckbox ? 'checkboxLabelBoxLeftSpace' : '']">
|
||||
<!--
|
||||
默认文本插槽
|
||||
@binding {boolean} checked - 是否选中
|
||||
-->
|
||||
<slot name="label" :checked="_isCheck">
|
||||
<tm-text :font-size="labelFontSize" class="checkboxLabel">{{ _label }}</tm-text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
transition-property: background-color;
|
||||
}
|
||||
|
||||
.checkboxLabelBox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkboxDisabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.checkboxBoxIcon {
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
transition-property: opacity, transform;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.checkboxBoxIcon_on {
|
||||
opacity: 1;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.checkboxBoxIcon_off {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.checkboxBox {
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkboxLabelBoxLeftSpace {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.checkboxLabel {
|
||||
font-size: 28rpx;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view @click="onClick" class="tmCol" :style="{ width: _span }">
|
||||
<!-- @slot 默认插槽 -->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ComputedRef, inject } from 'vue';
|
||||
/**
|
||||
* @displayName 布局Col
|
||||
* @exportName tm-col
|
||||
* @page tm-row
|
||||
* @category 常用组件
|
||||
* @description 快速排版布局利器,内只能放置在父组件tmRow中
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({
|
||||
name: "TmCol", options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
}
|
||||
});
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
'click',
|
||||
|
||||
])
|
||||
const attrs = defineProps({
|
||||
/**
|
||||
* 列宽,它是根据row定义的总列宽计算具体的宽度值。
|
||||
*/
|
||||
span: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
})
|
||||
const tmRowColWidth = inject<ComputedRef<number>>('tmRowColWidth', computed((): number => 5));
|
||||
const tmRowColGutter = inject<ComputedRef<string[]>>('tmRowColGutter', computed((): string[] => ['0px', '0px']));
|
||||
const _span = computed(() => {
|
||||
let col = typeof attrs.span == 'string' ? parseInt(attrs.span) : attrs.span
|
||||
let gutter = tmRowColGutter.value[1];
|
||||
if (gutter == '0rpx') {
|
||||
gutter = '0px'
|
||||
}
|
||||
if (col == tmRowColWidth.value) {
|
||||
return `100%`
|
||||
}
|
||||
return `calc(${col / tmRowColWidth.value * 100}% - ${gutter} + ${gutter} / ${tmRowColWidth.value})`;
|
||||
})
|
||||
const onClick = ()=>{
|
||||
emits('click');
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tmCol {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, provide, getCurrentInstance, computed, inject, onMounted } from 'vue'
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, setTextColorLightByDark, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import tmCollapse from '../tm-collapse/tm-collapse.vue';
|
||||
const proxy = getCurrentInstance()?.proxy;
|
||||
|
||||
/**
|
||||
* @displayName 折叠面板子组件
|
||||
* @exportName tm-collapse-item
|
||||
* @page tm-collapse
|
||||
* @category 展示组件
|
||||
* @description 可单,可多开,可放置在tm-collapse内
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmCollapse' });
|
||||
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 唯一标识
|
||||
*/
|
||||
name: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 是否显示底部边线
|
||||
*/
|
||||
showBottomLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 标题大小
|
||||
*/
|
||||
titleFontSize: {
|
||||
type: [String, Number],
|
||||
default: '30'
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
/**
|
||||
* 拒绝礼佛标题颜色,如果不填写取白
|
||||
*/
|
||||
darkTitleColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 激活时的颜色,空值读取全局值。
|
||||
*/
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 背景
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: 'white'
|
||||
},
|
||||
/**
|
||||
* 暗黑时的背景,如果不填写默认取sheetDarkColor
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 左边图标
|
||||
*/
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 标题高度
|
||||
*/
|
||||
titleHeight: {
|
||||
type: String,
|
||||
default: '100'
|
||||
},
|
||||
/**
|
||||
* 标题最多显示几行出现省略号
|
||||
*/
|
||||
titleLines: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/**
|
||||
* 下边线颜色.默认等同全局边线颜色.
|
||||
*/
|
||||
bottomColor: {
|
||||
type: String,
|
||||
default: '#f5f5f5'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const tmCollapseDefaultName = inject('tmCollapseDefaultName', computed((): Array<string | number> => []))
|
||||
const _disabled = computed(() => props.disabled)
|
||||
const _titleFontSize = computed(() => {
|
||||
let fontSize = covetUniNumber(props.titleFontSize, config.unit)
|
||||
if (config.fontSizeScale == 1) return fontSize
|
||||
let sizeNumber = parseInt(fontSize)
|
||||
if (isNaN(sizeNumber)) {
|
||||
sizeNumber = 16
|
||||
}
|
||||
return (sizeNumber * config.fontSizeScale).toString() + getUnit(fontSize)
|
||||
})
|
||||
const _titleHeight = computed(() => covetUniNumber(props.titleHeight, config.unit))
|
||||
const _titleColor = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkTitleColor != '') return getDefaultColor(props.darkTitleColor)
|
||||
return '#ffffff'
|
||||
}
|
||||
return getDefaultColor(props.titleColor)
|
||||
})
|
||||
const _activeColor = computed(() => {
|
||||
if (props.activeColor == "") return getDefaultColor(config.color)
|
||||
return getDefaultColor(props.activeColor)
|
||||
})
|
||||
const _color = computed(() => {
|
||||
if (config.mode == 'dark') {
|
||||
if (props.darkColor != '') return getDefaultColor(props.darkColor)
|
||||
return config.sheetDarkColor
|
||||
}
|
||||
return getDefaultColor(props.color)
|
||||
})
|
||||
const _bottomColor = computed(()=>{
|
||||
if(props.bottomColor != '') return getDefaultColor(props.bottomColor)
|
||||
if(config.mode == 'dark'){
|
||||
return getDefaultColor(config.borderDarkColor)
|
||||
}
|
||||
return getDefaultColor('#f5f5f5')
|
||||
})
|
||||
const _leftIcon = computed(() => props.leftIcon)
|
||||
const _title = computed(() => props.title)
|
||||
const _isActive = computed(() => tmCollapseDefaultName.value.includes(props.name))
|
||||
const _textMap = computed(() => {
|
||||
let styleMap = {} as Record<string, string>
|
||||
styleMap["fontSize"] = _titleFontSize.value
|
||||
styleMap["color"] = _isActive.value ? _activeColor.value : _titleColor.value
|
||||
styleMap["line-clamp"] = props.titleLines.toString()
|
||||
return styleMap
|
||||
})
|
||||
|
||||
function itemClick() {
|
||||
/**
|
||||
* 项目被点击时触发。
|
||||
* @param name {string} 当前name值
|
||||
*/
|
||||
emit('click', props.name)
|
||||
if(_disabled.value) return;
|
||||
let parent: InstanceType<typeof tmCollapse> | null = findParent(proxy)
|
||||
if (parent == null) return
|
||||
parent.toggleActivate(props.name, true)
|
||||
}
|
||||
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmCollapse") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmCollapse") return parents;
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmCollapseItemBox" :style="{ background: _color }">
|
||||
<view @click="itemClick" class="tmCollapseItem" :style="{ opacity: _disabled ? 0.5 : 1 }">
|
||||
<view class="tmCollapseItemBoxLeft">
|
||||
<tm-icon :size="_titleFontSize" v-if="_leftIcon" :name="_leftIcon"
|
||||
:color="_isActive ? _activeColor : _titleColor" _style="margin-right: 20rpx;"></tm-icon>
|
||||
<!--
|
||||
@slot 左边插槽
|
||||
@binding {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="left" :status="_isActive"></slot>
|
||||
|
||||
<view class="tmCollapseItemBoxTextBox" :style="{ height: _titleHeight }">
|
||||
<view style="flex:1">
|
||||
<!--
|
||||
@slot 标题插槽,如果你要完全自定标题样式请在此插槽内布局
|
||||
@binding {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="title" :status="_isActive">
|
||||
<text class="tmCollapseItemBoxText" :style="_textMap">
|
||||
{{ _title }}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="tmCollapseItemBoxRight">
|
||||
<!--
|
||||
@slot 右边插槽
|
||||
@binding {boolean} status - 当前展开状态
|
||||
-->
|
||||
<slot name="right" :status="_isActive"></slot>
|
||||
<tm-icon :color="_isActive ? _activeColor : '#bfbfbf'" _style="margin-left: 20rpx;"
|
||||
:name="_isActive ? 'arrow-down-s-line' : 'arrow-right-s-line'"></tm-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tmCollapseItemWrap" :class="[!_isActive ? 'tmCollapseItemWrapOff' : 'tmCollapseItemWrapOn']">
|
||||
<view class="tmCollapseItemContent">
|
||||
<!--
|
||||
@slot 默认内容插槽。
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view :style="{height:_isActive?'20rpx':'0px'}"</view>
|
||||
<view v-if="showBottomLine" :style="{height:'1px',background:_bottomColor}"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmCollapseItemBoxTextBox {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmCollapseItemBoxText {
|
||||
text-overflow: ellipsis;
|
||||
/* #ifndef APP */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.tmCollapseItemContent {
|
||||
/* padding: 12px 0rpx; */
|
||||
box-sizing: border-box;
|
||||
min-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tmCollapseItemBoxLeft {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmCollapseItemBoxRight {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmCollapseItemBox {
|
||||
/* background-color: white; */
|
||||
padding: 0px 12px;
|
||||
}
|
||||
|
||||
.tmCollapseItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.tmCollapseItemWrap {
|
||||
/* display: flex;
|
||||
flex-direction: column; */
|
||||
transition-property: all;
|
||||
transition-duration: 350ms;
|
||||
transition-timing-function: cubic-bezier(.18, .89, .32, 1);
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.tmCollapseItemWrapOff {
|
||||
grid-template-rows: 0fr;
|
||||
|
||||
}
|
||||
|
||||
.tmCollapseItemWrapOn {
|
||||
grid-template-rows: 1fr;
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, provide, onBeforeMount, computed } from "vue"
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, linearValid, getUnit, getUid } from "../../libs/tool"
|
||||
import { useTmConfig } from "../../libs/config"
|
||||
import { getDefaultColor, setTextColorLightByDark, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors"
|
||||
|
||||
/**
|
||||
* @displayName 折叠面板
|
||||
* @exportName tm-collapse
|
||||
* @category 展示组件
|
||||
* @description 可单,可多开,内部可放置tm-collapse-item子节点组件,动画采用了新css,兼容性请看这里:[点我](https://caniuse.com/?search=grid-template-rows)
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmCollapse" })
|
||||
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前子项值。可v-model,类型兼容性高,因此不管你提供的是不是数组,最终同步时会被设置成数组
|
||||
*/
|
||||
modelValue: {
|
||||
type: [Array<string | number>, String, Number],
|
||||
default: (): string[] => [] as string[],
|
||||
},
|
||||
/**
|
||||
* 是否允许打开多个。
|
||||
*/
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 变换时触发
|
||||
* @param {String[]} value - 当前打开的值
|
||||
*/
|
||||
"change",
|
||||
"update:modelValue",
|
||||
])
|
||||
|
||||
const activeName = ref<Array<string | number>>([])
|
||||
|
||||
onBeforeMount(() => {
|
||||
toggleActivate(props.modelValue)
|
||||
})
|
||||
|
||||
provide(
|
||||
"tmCollapseDefaultName",
|
||||
computed((): Array<string | number> => activeName.value),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue: Array<string | number> | string | number) => {
|
||||
if (newValue == activeName.value.join("")) return
|
||||
if (Array.isArray(newValue)) {
|
||||
if (newValue.join("") == activeName.value.join("")) return
|
||||
}
|
||||
toggleActivate(newValue)
|
||||
},
|
||||
)
|
||||
/**
|
||||
* 切换选中
|
||||
*/
|
||||
const toggleActivate = (name: Array<string | number> | string | number, isEmits = false) => {
|
||||
let ar = [] as Array<string | number>
|
||||
if (Array.isArray(name)) {
|
||||
ar = name
|
||||
} else if (name !== "" && name != undefined && name != null) {
|
||||
ar = [name]
|
||||
}
|
||||
|
||||
if (ar.length == 0) {
|
||||
activeName.value = []
|
||||
} else {
|
||||
if (props.multiple) {
|
||||
let diff = activeName.value.filter((item: string | number) => {
|
||||
return ar.indexOf(item) == -1
|
||||
})
|
||||
let diffname = ar.filter((item: string | number) => {
|
||||
return activeName.value.indexOf(item) == -1
|
||||
})
|
||||
activeName.value = [...diff, ...diffname].slice(0)
|
||||
} else {
|
||||
if (activeName.value.indexOf(ar[0]) != -1) {
|
||||
activeName.value = []
|
||||
} else {
|
||||
activeName.value = [ar[0]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmits) {
|
||||
let covervalue: string | number | Array<string | number> = activeName.value.slice(0)
|
||||
emit("update:modelValue", covervalue)
|
||||
emit("change", covervalue)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ toggleActivate })
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<!--
|
||||
@slot 默认插槽,可放置tm-collapse-item子节点
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,470 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch, type PropType } from 'vue';
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { tmDate, type tmDateTypeTime, createDate } from '../../libs/tmDate';
|
||||
import pickerItem from './../tm-picker-view/picker-item.vue';
|
||||
type PICKER_ITEM_INFO = Record<string, any>
|
||||
|
||||
/**
|
||||
* @displayName 日期选择容器
|
||||
* @exportName tm-date-view
|
||||
* @page tm-picker-date
|
||||
* @category 表单组件
|
||||
* @description 如果需要弹层式用tm-picker-date组件,这个是展示型.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmDateView' });
|
||||
const { config } = useTmConfig()
|
||||
|
||||
type coverValue = {
|
||||
value: string[],
|
||||
str: string
|
||||
}
|
||||
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
/**
|
||||
* @name 日期选择器 tmPickerDate
|
||||
* @description 日期选择,可以控制显示精确到秒。默认的开始时间为当前时间的上一年,结束时间为默认当前时间
|
||||
* 使用时,建议不要显示过多年份以防卡太多数据。
|
||||
* @page /pages/index/picker-date
|
||||
* @category 表单组件
|
||||
* @constant 平台兼容
|
||||
* | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| ☑ | ☑️ | ☑️ | x | ☑️ | 4.14+ | 1.0.0 |
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
|
||||
* 否则报错,无法运行。
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 当前时间经过format格式化后输出的值。
|
||||
* 此值不会处理输入,只输出显示。
|
||||
*/
|
||||
modelStr: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 顶部标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "请选择时间"
|
||||
},
|
||||
/**
|
||||
* 开始时间,请提供正确的时间格式
|
||||
*/
|
||||
start: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 结束时间,请提供正确的时间格式
|
||||
*/
|
||||
end: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 精确到的级别
|
||||
* year:年
|
||||
* month:年月
|
||||
* day:年月日
|
||||
* hour:年月日小时
|
||||
* minute:年月日小时分钟
|
||||
* second:年月日小时分钟秒
|
||||
*/
|
||||
type: {
|
||||
type: String as PropType<ModelType>,
|
||||
default: "day"
|
||||
},
|
||||
/**
|
||||
* 输出时间格式,只对v-model:modelStr有效
|
||||
* 有效格式:
|
||||
* YYYY年
|
||||
* MM月
|
||||
* DD日
|
||||
* hh小时
|
||||
* mm分钟
|
||||
* ss秒
|
||||
*/
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD"
|
||||
},
|
||||
/**
|
||||
* 上方的单位名称
|
||||
*/
|
||||
cellUnits: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => ['年', '月', '日', '时', '分', '秒']
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 滑动变换时触发
|
||||
* @param {string} date - 当前选中时间
|
||||
*/
|
||||
'change',
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
'update:modelStr',
|
||||
'update:modelValue'
|
||||
]);
|
||||
|
||||
const nowValue = ref<string[]>([]);
|
||||
const _modelValueIndex = ref<number[]>([])
|
||||
const nowValueStr = ref('');
|
||||
const startDate = ref(new tmDate().subtraction(1, 'y'));
|
||||
const endDate = ref(new tmDate());
|
||||
const dateList = ref<PICKER_ITEM_INFO[][]>([]);
|
||||
const _start_date = computed(() => props.start ? new tmDate(props.start) : startDate.value);
|
||||
const _end_date = computed(() => props.end ? new tmDate(props.end) : endDate.value);
|
||||
const _getDateType = computed((): tmDateTypeTime => {
|
||||
let isType: tmDateTypeTime = 's';
|
||||
if (props.type == 'year') isType = 'y';
|
||||
if (props.type == 'month') isType = 'm';
|
||||
if (props.type == 'day') isType = 'd';
|
||||
if (props.type == 'hour') isType = 'h';
|
||||
if (props.type == 'minute') isType = 'M';
|
||||
return isType;
|
||||
});
|
||||
|
||||
const defaultModelvalue = (newvalue: string, showStr: boolean) => {
|
||||
let isType = _getDateType.value;
|
||||
let selfnowValue = new tmDate(newvalue);
|
||||
if (selfnowValue.isBetweenOf(_start_date.value, '<=', isType)) {
|
||||
selfnowValue = _start_date.value;
|
||||
}
|
||||
if (selfnowValue.isBetweenOf(_end_date.value, '>=', isType)) {
|
||||
|
||||
selfnowValue = _end_date.value;
|
||||
}
|
||||
|
||||
let stp = getRangByDateTime(selfnowValue);
|
||||
nowValue.value = stp.value;
|
||||
nowValueStr.value = stp.str;
|
||||
dateList.value = getTimeTreeByStartAndEnd(_start_date.value, _end_date.value);
|
||||
_modelValueIndex.value = getIndexsByids(stp.value)
|
||||
|
||||
if (showStr) {
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', formatTimeDate());
|
||||
}
|
||||
}
|
||||
|
||||
const getIndexsByids = (ids: string[]) => {
|
||||
let indexs = [] as number[];
|
||||
for (let i = 0; i < dateList.value.length; i++) {
|
||||
let item = dateList.value[i]
|
||||
let index = item.findIndex(el => parseInt(el.id) == parseInt(ids[i]))
|
||||
indexs.push(Math.max(0, index))
|
||||
}
|
||||
return indexs
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const getTimeTreeByStartAndEnd = (start: tmDate, end: tmDate): PICKER_ITEM_INFO[][] => {
|
||||
let startCopy = start.getClone();
|
||||
let nowDate = new tmDate(nowValueStr.value)
|
||||
let endCopy = end.getClone();
|
||||
let years = [] as PICKER_ITEM_INFO[];
|
||||
let months = [] as PICKER_ITEM_INFO[];
|
||||
let days = [] as PICKER_ITEM_INFO[];
|
||||
let hours = [] as PICKER_ITEM_INFO[];
|
||||
let minutes = [] as PICKER_ITEM_INFO[];
|
||||
let seconds = [] as PICKER_ITEM_INFO[];
|
||||
|
||||
for (let i = startCopy.getYear(); i <= endCopy.getYear(); i++) {
|
||||
years.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
|
||||
function getD(type: string, s: number, n: number) {
|
||||
if (type == 'm') {
|
||||
|
||||
for (let i = s; i <= n; i++) {
|
||||
months.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i + 1)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'd') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
days.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'h') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
hours.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 'M') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
minutes.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
} else if (type == 's') {
|
||||
for (let i = s; i <= n; i++) {
|
||||
seconds.push({
|
||||
id: i.toString(),
|
||||
title: indexToHex(i)
|
||||
} as PICKER_ITEM_INFO)
|
||||
}
|
||||
}
|
||||
}
|
||||
function getDnumber(type: tmDateTypeTime, target: tmDateTypeTime): number[] {
|
||||
let st = 0;
|
||||
let et = 0
|
||||
|
||||
// 不包含起始, 开始和结束内。
|
||||
if (nowDate.isBetween(startCopy, endCopy, type, '()')) {
|
||||
if (target == 'm') {
|
||||
st = 0
|
||||
et = 11
|
||||
} else if (target == 'd') {
|
||||
st = 1
|
||||
et = nowDate.getMonthCountDay()
|
||||
} else if (target == 'h') {
|
||||
st = 0
|
||||
et = 23
|
||||
} else if (target == 'M' || target == 's') {
|
||||
st = 0
|
||||
et = 59
|
||||
}
|
||||
} else {
|
||||
// 开始和结束相等
|
||||
if (startCopy.isBetweenOf(endCopy, '=', type)) {
|
||||
|
||||
if (target == 'm') {
|
||||
st = startCopy.getMonth()
|
||||
et = endCopy.getMonth()
|
||||
} else if (target == 'd') {
|
||||
st = startCopy.getDate()
|
||||
et = endCopy.getDate()
|
||||
} else if (target == 'h') {
|
||||
st = startCopy.getHours()
|
||||
et = endCopy.getHours()
|
||||
} else if (target == 'M') {
|
||||
st = startCopy.getMinutes()
|
||||
et = endCopy.getMinutes()
|
||||
} else if (target == 's') {
|
||||
st = startCopy.getSeconds()
|
||||
et = endCopy.getSeconds()
|
||||
}
|
||||
|
||||
} else if (nowDate.isBetweenOf(startCopy, '<=', type)) {
|
||||
|
||||
if (target == 'm') {
|
||||
st = startCopy.getMonth()
|
||||
et = 11
|
||||
} else if (target == 'd') {
|
||||
st = startCopy.getDate()
|
||||
et = startCopy.getMonthCountDay()
|
||||
} else if (target == 'h') {
|
||||
st = startCopy.getHours()
|
||||
et = 23
|
||||
} else if (target == 'M') {
|
||||
st = startCopy.getMinutes()
|
||||
et = 59
|
||||
} else if (target == 's') {
|
||||
st = startCopy.getSeconds()
|
||||
et = 59
|
||||
}
|
||||
} else if (nowDate.isBetweenOf(endCopy, '>=', type)) {
|
||||
|
||||
if (target == 'm') {
|
||||
st = 0
|
||||
et = endCopy.getMonth()
|
||||
} else if (target == 'd') {
|
||||
st = 1
|
||||
et = endCopy.getDate()
|
||||
} else if (target == 'h') {
|
||||
st = 0
|
||||
et = endCopy.getHours()
|
||||
} else if (target == 'M') {
|
||||
st = 0
|
||||
et = endCopy.getMinutes()
|
||||
} else if (target == 's') {
|
||||
st = 0
|
||||
et = endCopy.getSeconds()
|
||||
}
|
||||
}
|
||||
}
|
||||
return [st, et] as number[]
|
||||
}
|
||||
let maxlen = getNowTypeLenIndex();
|
||||
|
||||
if (maxlen > 1) {
|
||||
let sdate = getDnumber('y', 'm')
|
||||
|
||||
getD('m', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 2) {
|
||||
let sdate = getDnumber('m', 'd')
|
||||
getD('d', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 3) {
|
||||
let sdate = getDnumber('d', 'h')
|
||||
getD('h', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 4) {
|
||||
let sdate = getDnumber('h', 'M')
|
||||
getD('M', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
if (maxlen > 5) {
|
||||
let sdate = getDnumber('m', 's')
|
||||
getD('s', sdate[0]!, sdate[1]!)
|
||||
}
|
||||
|
||||
|
||||
return [years, months, days, hours, minutes, seconds].slice(0, getNowTypeLenIndex()) as PICKER_ITEM_INFO[][];
|
||||
}
|
||||
const indexToHex = (i: number) => {
|
||||
let n = i.toString()
|
||||
if (n.length == 1) return '0' + n;
|
||||
return n;
|
||||
}
|
||||
const getRangByDateTime = (d: tmDate) => {
|
||||
let nowRange = [
|
||||
d.getYear().toString(),
|
||||
(d.getMonth()).toString(),
|
||||
d.getDate().toString(),
|
||||
d.getHours().toString(),
|
||||
d.getMinutes().toString(),
|
||||
d.getSeconds().toString(),
|
||||
] as string[];
|
||||
let nowRangeStr = d.getYear().toString() + "-" + (d.getMonth() + 1).toString()
|
||||
+ "-" + d.getDate().toString()
|
||||
+ " " + d.getHours().toString() + ":"
|
||||
+ d.getMinutes().toString() + ":"
|
||||
+ d.getSeconds().toString();
|
||||
return {
|
||||
value: nowRange as string[],
|
||||
str: nowRangeStr
|
||||
} as coverValue;
|
||||
}
|
||||
|
||||
const getNowTypeLenIndex = () => {
|
||||
let index = 6
|
||||
if (props.type == 'year') {
|
||||
index = 1;
|
||||
} else if (props.type == 'month') {
|
||||
index = 2;
|
||||
} else if (props.type == 'day') {
|
||||
index = 3;
|
||||
} else if (props.type == 'hour') {
|
||||
index = 4;
|
||||
} else if (props.type == 'minute') {
|
||||
index = 5;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
const fillNumber = (n: string) => {
|
||||
if (parseInt(n) > 9) return n;
|
||||
return "0" + n
|
||||
}
|
||||
const stringArValuCoverToString = () => {
|
||||
if (nowValue.value.length != 6) return "";
|
||||
|
||||
let newsday = new tmDate(nowValue.value[0] + "-" + (parseInt(nowValue.value[1]) + 1).toString() + "-1")
|
||||
let days = parseInt(nowValue.value[2])
|
||||
days = days >= newsday.getMonthCountDay() ? newsday.getMonthCountDay() : days
|
||||
nowValue.value.splice(2, 1, days.toString())
|
||||
|
||||
return fillNumber(nowValue.value[0]) + "-" + fillNumber((parseInt(nowValue.value[1]) + 1).toString())
|
||||
+ "-" + fillNumber(nowValue.value[2])
|
||||
+ " " + fillNumber(nowValue.value[3]) + ":"
|
||||
+ fillNumber(nowValue.value[4]) + ":"
|
||||
+ fillNumber(nowValue.value[5])
|
||||
}
|
||||
|
||||
|
||||
const change = (ixs: number[]) => {
|
||||
let nowvalstrs = [] as string[]
|
||||
for (let i = 0; i < ixs.length; i++) {
|
||||
let item = dateList.value[i]
|
||||
nowvalstrs.push(item[ixs[i]].title)
|
||||
}
|
||||
|
||||
let str = nowvalstrs.join('/');
|
||||
let stp = getRangByDateTime(new tmDate(str));
|
||||
|
||||
defaultModelvalue(stp.str, true)
|
||||
|
||||
onConfirm()
|
||||
}
|
||||
|
||||
|
||||
const formatTimeDate = () => {
|
||||
if (nowValue.value.length != 6) return "";
|
||||
let sp = props.format;
|
||||
sp = sp.replace(/YYYY/g, fillNumber(nowValue.value[0]))
|
||||
sp = sp.replace(/MM/g, fillNumber((parseInt(nowValue.value[1]) + 1).toString()))
|
||||
sp = sp.replace(/DD/g, fillNumber(nowValue.value[2]))
|
||||
sp = sp.replace(/hh/g, fillNumber(nowValue.value[3]))
|
||||
sp = sp.replace(/mm/g, fillNumber(nowValue.value[4]))
|
||||
sp = sp.replace(/ss/g, fillNumber(nowValue.value[5]))
|
||||
return sp;
|
||||
}
|
||||
const onConfirm = () => {
|
||||
nowValueStr.value = stringArValuCoverToString();
|
||||
/**
|
||||
* 点击确认时同步。等同v-model
|
||||
*/
|
||||
emit('update:modelValue', nowValueStr.value);
|
||||
|
||||
/**
|
||||
* 经格式化后的值。等同v-model:model-str
|
||||
*/
|
||||
emit('update:modelStr', formatTimeDate());
|
||||
|
||||
emit('change', nowValueStr.value)
|
||||
}
|
||||
|
||||
|
||||
watch(() => props.modelValue, (newvalue: string) => {
|
||||
if (newvalue == '') return;
|
||||
let isType = _getDateType.value;
|
||||
if (new tmDate(newvalue).isBetweenOf(new tmDate(nowValueStr.value), '=', isType)) return;
|
||||
defaultModelvalue(new tmDate(newvalue).format('YYYY/MM/DD HH:mm:ss'), true);
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
let nowValue = new tmDate(props.modelValue);
|
||||
defaultModelvalue(nowValue.format('YYYY/MM/DD HH:mm:ss'), props.modelValue != '');
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<picker-item @change-deep="change" :selected-index="_modelValueIndex" :list="dateList"
|
||||
:cell-units="cellUnits.slice(0, dateList.length)"></picker-item>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,773 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, onUpdated, nextTick } from "vue"
|
||||
import {
|
||||
arrayNumberValid,
|
||||
arrayNumberValidByStyleMP,
|
||||
covetUniNumber,
|
||||
arrayNumberValidByStyleBorderColor,
|
||||
linearValid,
|
||||
getUnit,
|
||||
getUid,
|
||||
} from "../../libs/tool"
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors"
|
||||
import { useTmConfig } from "../../libs/config"
|
||||
import { onPageScroll } from "@dcloudio/uni-app"
|
||||
|
||||
/**
|
||||
* @displayName 抽屉
|
||||
* @exportName tm-drawer
|
||||
* @category 反馈组件
|
||||
* @description 提供四个方向的弹出。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmDrawer" })
|
||||
const { config } = useTmConfig()
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 自定义遮罩样式
|
||||
*/
|
||||
customStyle: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题",
|
||||
},
|
||||
/**
|
||||
* 显示底部操作栏
|
||||
*/
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 是否显示标题
|
||||
*/
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 是否显示底部关闭按钮
|
||||
*/
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 遮罩是否允许点击被关闭
|
||||
*/
|
||||
overlayClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 显示可v-model:show双向绑定
|
||||
*/
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 显示取消按钮
|
||||
*/
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 取消按钮的文本
|
||||
*/
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: "取消",
|
||||
},
|
||||
/**
|
||||
* 确认按钮的文本
|
||||
*/
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: "确认",
|
||||
},
|
||||
/**
|
||||
* 动画时间
|
||||
*/
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
/**
|
||||
* 打开方向。
|
||||
*/
|
||||
position: {
|
||||
type: String as PropType<"top" | "bottom" | "left" | "right">,
|
||||
default: "bottom",
|
||||
},
|
||||
/**
|
||||
* 打开方向为上和下时的圆角
|
||||
* 空值时,取全局配置的圆角。
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 左右时为内容宽,
|
||||
* 上下时为内容高
|
||||
* 百分比,数字字符或者带单位,或者为auto(根据内容自动高度或者宽高)
|
||||
*/
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: "50%",
|
||||
},
|
||||
/**
|
||||
* 弹层最大的高度值,默认为屏幕的可视高
|
||||
* 提供值时不能为百分比,可以是px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
|
||||
*/
|
||||
maxHeight: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
/**
|
||||
* 暗黑背景颜色,如果不提供默认读取全局的sheet配置
|
||||
*/
|
||||
darkBgColor: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否禁用内部的scroll标签
|
||||
* 禁用后内容不会滚动,如果设定了指定高,内容超出指定高,会被裁切
|
||||
* 但如果没有指定高,内容自动的话,高是自动的。
|
||||
*/
|
||||
disabledScroll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 内容区域左右和下的边距。
|
||||
*/
|
||||
contentMargin: {
|
||||
type: String,
|
||||
default: "24",
|
||||
},
|
||||
/**
|
||||
* 宽屏时是否让内容剧中显示
|
||||
* 并限制其它宽为屏幕宽,只展示中间内容以适应宽屏。
|
||||
* 注意只有top,bottom才会生效。
|
||||
*/
|
||||
widthCoverCenter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 距离顶部的偏移量
|
||||
*/
|
||||
offsetTop: {
|
||||
type: [String, Number],
|
||||
default: "0",
|
||||
},
|
||||
/**
|
||||
* 弹层的层级
|
||||
*/
|
||||
zIndex: {
|
||||
type: [String, Number],
|
||||
default: 1100,
|
||||
},
|
||||
/**
|
||||
* 懒加载
|
||||
* 为了解决业务布局节点超多时,你可能需要内容延迟加载以免阻塞动画流畅度.
|
||||
* 如果你启用了lazy,每天次打开时,动画执行后才会显示内容.这样动画就流畅,不会因为节点过多造成的卡.
|
||||
*/
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 关闭前执行,可以是异步函数,只有当用户点击confirm时才会触发。如果返回false将阻止关闭。true允许关闭。
|
||||
*/
|
||||
beforeClose: {
|
||||
type: [Function, Boolean] as PropType<() => Promise<boolean>>,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
"click",
|
||||
/**
|
||||
* 关闭是触发
|
||||
*/
|
||||
"close",
|
||||
/**
|
||||
* 打开时触发
|
||||
*/
|
||||
"open",
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
"beforeOpen",
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
"beforeClose",
|
||||
/**
|
||||
* 等同v-model:show
|
||||
*/
|
||||
"update:show",
|
||||
/**
|
||||
* 取消时触发
|
||||
*/
|
||||
"cancel",
|
||||
/**
|
||||
* 确认时触发
|
||||
*/
|
||||
"confirm",
|
||||
])
|
||||
|
||||
const isLoading = ref(false)
|
||||
const _width = ref(0)
|
||||
const _height = ref(0)
|
||||
const showOverflay = ref(false)
|
||||
const actinon = ref(false)
|
||||
const status = ref("")
|
||||
const id = ref("tmDrawer" + getUid())
|
||||
const wrapId = ref("tmDrawerWrap" + getUid())
|
||||
const first = ref(true)
|
||||
const tid = ref(10)
|
||||
const tid2 = ref(0)
|
||||
const windtop = ref(0)
|
||||
const windtopReal = ref(0)
|
||||
const tantiaoTrue = ref(false)
|
||||
const safeFooterHeight = ref(0)
|
||||
const lezyShowModal = ref(props.lazy ? false : true)
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
showAlert()
|
||||
} else {
|
||||
closeAlert()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const _lazy = computed(() => props.lazy)
|
||||
const _customStyle = computed(() => props.customStyle)
|
||||
const _show = computed(() => props.show)
|
||||
const _widthCoverCenter = computed(() => props.widthCoverCenter)
|
||||
const _showClose = computed(() => props.showClose)
|
||||
const _duration = computed(() => props.duration)
|
||||
const _position = computed(() => props.position)
|
||||
const _title = computed(() => props.title)
|
||||
const _showTitle = computed(() => props.showTitle)
|
||||
const _round = computed(() => {
|
||||
let round = props.round
|
||||
if (round == "") {
|
||||
round = config.drawerRadius
|
||||
}
|
||||
let radius = covetUniNumber(round, config.unit)
|
||||
|
||||
let _r = "none"
|
||||
if (props.position == "top") {
|
||||
_r = `0px 0px ${radius} ${radius}`
|
||||
}
|
||||
if (props.position == "bottom") {
|
||||
_r = `${radius} ${radius} 0px 0px`
|
||||
}
|
||||
return _r
|
||||
})
|
||||
const _offset = computed(() => {
|
||||
let offset = covetUniNumber(props.offsetTop, config.unit)
|
||||
let x = parseFloat(offset)
|
||||
let unit = getUnit(offset)
|
||||
if (unit == "rpx") {
|
||||
x = uni.upx2px(x)
|
||||
}
|
||||
return x
|
||||
})
|
||||
const _size = computed(() => covetUniNumber(props.size, config.unit))
|
||||
const _contentMargin = computed(() => covetUniNumber(props.contentMargin, config.unit))
|
||||
const _showFooter = computed(() => props.showFooter)
|
||||
const _maxHeight = computed(() => {
|
||||
if (props.maxHeight == "") return ""
|
||||
if (props.position == "left" || props.position == "right") return ""
|
||||
|
||||
return covetUniNumber(props.maxHeight, config.unit)
|
||||
})
|
||||
const _showCancel = computed(() => props.showCancel)
|
||||
const _cancelText = computed(() => props.cancelText)
|
||||
const _confirmText = computed(() => props.confirmText)
|
||||
const _animationFun = computed(() => config.animation)
|
||||
const _bgColor = computed(() => {
|
||||
let bgcolor = props.bgColor
|
||||
if (config.mode == "dark") {
|
||||
if (props.darkBgColor != "") {
|
||||
bgcolor = props.darkBgColor
|
||||
} else {
|
||||
bgcolor = config.sheetDarkColor
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor)
|
||||
})
|
||||
const __height = computed(() => {
|
||||
let h = "100%"
|
||||
// #ifdef WEB
|
||||
h = `calc(100% - ${windtopReal.value}px)`
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
if (_offset.value > 0) {
|
||||
h = _height.value + "px"
|
||||
}
|
||||
// #endif
|
||||
return h
|
||||
})
|
||||
const _titleFontSize = computed(() => (config.fontSizeScale * 16).toString() + "px")
|
||||
const _isDark = computed(() => config.mode == "dark")
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
lezyShowModal.value = props.lazy ? false : true
|
||||
let sys = uni.getWindowInfo()
|
||||
// #ifdef WEB
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight
|
||||
|
||||
windtop.value = sys.windowTop + (_position.value == "bottom" ? 0 : _offset.value)
|
||||
windtopReal.value = _position.value == "bottom" ? sys.windowTop + _offset.value : sys.windowTop
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
_width.value = sys.windowWidth
|
||||
_height.value = sys.windowHeight
|
||||
windtop.value = _offset.value
|
||||
// #endif
|
||||
safeFooterHeight.value = sys.safeAreaInsets.bottom == 0 ? 16 : sys.safeAreaInsets.bottom
|
||||
if (props.show) {
|
||||
showAlert()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value)
|
||||
clearTimeout(tid2.value)
|
||||
})
|
||||
|
||||
const cancelEvt = () => {
|
||||
emit("cancel")
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const confirmEvt = async () => {
|
||||
emit("confirm")
|
||||
let isPass = true
|
||||
if (typeof props.beforeClose == "function") {
|
||||
isLoading.value = true
|
||||
// @ts-ignore
|
||||
isPass = await props.beforeClose()
|
||||
}
|
||||
isLoading.value = false
|
||||
if (!isPass) {
|
||||
return
|
||||
}
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const onClickOverflowy = (evt: Event) => {
|
||||
evt.stopPropagation()
|
||||
/**
|
||||
* 点击遮罩事件
|
||||
*/
|
||||
emit("click")
|
||||
if (isLoading.value) return
|
||||
if (!props.overlayClick) {
|
||||
clearTimeout(tid2.value)
|
||||
tantiaoTrue.value = true
|
||||
tid2.value = setTimeout(() => {
|
||||
tantiaoTrue.value = false
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
closeAlert()
|
||||
}
|
||||
|
||||
const closeAlert = () => {
|
||||
if (actinon.value || isLoading.value) return
|
||||
if (status.value == "close") return
|
||||
actinon.value = true
|
||||
status.value = "close"
|
||||
/**
|
||||
* 关闭前执行
|
||||
*/
|
||||
emit("beforeClose")
|
||||
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(() => {
|
||||
onEnd()
|
||||
}, _duration.value)
|
||||
}
|
||||
|
||||
const showAlert = () => {
|
||||
if (actinon.value) return
|
||||
if (status.value == "open") return
|
||||
showOverflay.value = true
|
||||
actinon.value = true
|
||||
|
||||
/**
|
||||
* 打开前执行
|
||||
*/
|
||||
emit("beforeOpen")
|
||||
clearTimeout(tid.value)
|
||||
tid.value = setTimeout(() => {
|
||||
status.value = "open"
|
||||
tid.value = setTimeout(() => {
|
||||
onEnd()
|
||||
}, _duration.value)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
showAlert()
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
actinon.value = false
|
||||
|
||||
if (status.value === "close") {
|
||||
showOverflay.value = false
|
||||
emit("close")
|
||||
emit("update:show", false)
|
||||
if (_lazy.value) {
|
||||
lezyShowModal.value = false
|
||||
}
|
||||
} else {
|
||||
emit("open")
|
||||
if (_lazy.value) {
|
||||
lezyShowModal.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maskerMove(evt: TouchEvent) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<view>
|
||||
<view @click="openDrawer">
|
||||
<!--
|
||||
标签触发显示遮罩,免于使用变量控制
|
||||
@slot {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="trigger" :show="show"></slot>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<teleport to="uni-app">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<root-portal>
|
||||
<!-- #endif -->
|
||||
<view
|
||||
@click="onClickOverflowy"
|
||||
@touchmove="maskerMove"
|
||||
v-if="showOverflay"
|
||||
:id="id"
|
||||
ref="tmDrawerWrap"
|
||||
class="tmDrawerWrap"
|
||||
:class="[
|
||||
status == 'open' ? 'tmModalWrap_on' : 'tmModalWrap_off',
|
||||
(_position == 'top' || _position == 'bottom') && _widthCoverCenter ? 'tmDrawerWrapContentMinwidthWrapDir' : '',
|
||||
'tmDrawerWrap_' + _position,
|
||||
]"
|
||||
:style="[
|
||||
{
|
||||
width: '100%',
|
||||
top: windtop + 'px',
|
||||
height: __height,
|
||||
zIndex: zIndex,
|
||||
'transition-timing-function': _animationFun,
|
||||
},
|
||||
_customStyle,
|
||||
]"
|
||||
>
|
||||
<view
|
||||
@click.stop=""
|
||||
ref="tmDrawerWrapContent"
|
||||
class="tmDrawerWrapContent"
|
||||
:class="[
|
||||
(_position == 'top' || _position == 'bottom') && _widthCoverCenter ? 'tmDrawerWrapContentMinwidth' : '',
|
||||
'tmDrawerWrapContent_' + _position + (status == 'open' ? '_on' : ''),
|
||||
tantiaoTrue ? 'tmModalWrapContent_Tantiao' : '',
|
||||
]"
|
||||
:id="wrapId"
|
||||
:style="{
|
||||
width: _position == 'left' || _position == 'right' ? _size : '100%',
|
||||
height: _position == 'left' || _position == 'right' ? '100%' : _size,
|
||||
borderRadius: _round,
|
||||
maxHeight: _maxHeight != '' ? _maxHeight : '100%',
|
||||
minHeight: _size == 'auto' ? '350rpx' : '0px',
|
||||
'transition-timing-function': _animationFun,
|
||||
backgroundColor: _bgColor,
|
||||
}"
|
||||
>
|
||||
<view v-if="_showClose" class="tmDrawerXclose">
|
||||
<tm-icon @click="closeAlert" color="#dcdcdc" size="52" name="close-circle-fill"></tm-icon>
|
||||
</view>
|
||||
<view>
|
||||
<view v-if="_showTitle" class="tmDrawerTitleBox">
|
||||
<!--
|
||||
标题插槽
|
||||
@slot {Boolean} show - 当前是否已显示
|
||||
-->
|
||||
<slot name="title" :show="show">
|
||||
<text
|
||||
:style="{ fontSize: _titleFontSize, color: _isDark ? 'white' : 'black', opacity: '0.64' }"
|
||||
class="tmDrawertitleBox"
|
||||
>{{ _title }}</text
|
||||
>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!disabledScroll && _size != 'auto'" style="flex: 1" class="tmModalWrapBoxContent">
|
||||
<scroll-view style="height: 100%; position: absolute; width: 100%" :scroll-y="true" :rebound="false">
|
||||
<view :style="{ padding: `0px ${_contentMargin} 0px ${_contentMargin}` }">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view v-else :style="{ height: '100%', padding: `0px ${_contentMargin} 0px ${_contentMargin}` }">
|
||||
<!--
|
||||
@slot 默认插槽
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
|
||||
<view v-if="showFooter && lezyShowModal" class="tmDrawerFooter" :style="{ backgroundColor: _bgColor }">
|
||||
<view :style="{ height: _contentMargin }"></view>
|
||||
|
||||
<!--
|
||||
@slot 底部操作栏
|
||||
-->
|
||||
<slot name="footer">
|
||||
<view style="flex-direction: row; align-items: center; justify-content: center; display: flex; margin: 0 20rpx">
|
||||
<tm-button
|
||||
:loading="isLoading"
|
||||
width="0px"
|
||||
@click="cancelEvt"
|
||||
v-if="_showCancel"
|
||||
skin="thin"
|
||||
style="margin-right: 16px; flex: 1"
|
||||
>{{ _cancelText }}</tm-button
|
||||
>
|
||||
<tm-button :loading="isLoading" width="0px" @click="confirmEvt" style="flex: 1">{{ _confirmText }}</tm-button>
|
||||
</view>
|
||||
</slot>
|
||||
<view :style="{ height: safeFooterHeight + 'px' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
</root-portal>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
</teleport>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.tmDrawerFooter {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmDrawerXclose {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 18rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tmDrawerTitleBox {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tmDrawertitleBox {
|
||||
max-width: 175px;
|
||||
overflow: hidden;
|
||||
line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tmDrawerWrap_right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent {
|
||||
transition-duration: 350ms;
|
||||
transition-property: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContentMinwidth {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContentMinwidthWrapDir {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_bottom {
|
||||
transform: translate(0%, 100%);
|
||||
}
|
||||
|
||||
@keyframes bottomAnimate3d {
|
||||
0% {
|
||||
transform: translate(0%, 100%) translate3d(0%, 20px, 0px) rotateX(45deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0%, 0%) translate3d(0, 0, 0px) rotateX(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_bottom_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_top {
|
||||
transform: translate(0%, -100%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_top_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_left {
|
||||
transform: translate(-100%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_left_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_right {
|
||||
transform: translate(100%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrapContent_right_on {
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
.tmDrawerWrap {
|
||||
opacity: 1;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
transition-duration: 350ms;
|
||||
transition-property: background-color;
|
||||
perspective: 1000px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.tmModalWrap_on {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.tmModalWrap_off {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.tmModalWrapBoxContent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes tantiao {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tmModalWrapContent_Tantiao {
|
||||
animation: tantiao 0.15s linear;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
// @ts-nocheck
|
||||
export default class WxCanvas {
|
||||
constructor(ctx, canvasId, isNew, canvasNode) {
|
||||
this.ctx = ctx;
|
||||
this.canvasId = canvasId;
|
||||
this.chart = null;
|
||||
this.isNew = isNew
|
||||
if (isNew) {
|
||||
this.canvasNode = canvasNode;
|
||||
}
|
||||
else {
|
||||
this._initStyle(ctx);
|
||||
}
|
||||
|
||||
// this._initCanvas(zrender, ctx);
|
||||
|
||||
this._initEvent();
|
||||
}
|
||||
|
||||
getContext(contextType) {
|
||||
if (contextType === '2d') {
|
||||
return this.ctx;
|
||||
}
|
||||
}
|
||||
|
||||
// canvasToTempFilePath(opt) {
|
||||
// if (!opt.canvasId) {
|
||||
// opt.canvasId = this.canvasId;
|
||||
// }
|
||||
// return wx.canvasToTempFilePath(opt, this);
|
||||
// }
|
||||
|
||||
setChart(chart) {
|
||||
this.chart = chart;
|
||||
}
|
||||
|
||||
attachEvent() {
|
||||
// noop
|
||||
}
|
||||
|
||||
detachEvent() {
|
||||
// noop
|
||||
}
|
||||
|
||||
_initCanvas(zrender, ctx) {
|
||||
zrender.util.getContext = function () {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
zrender.util.$override('measureText', function (text, font) {
|
||||
ctx.font = font || '12px sans-serif';
|
||||
return ctx.measureText(text);
|
||||
});
|
||||
}
|
||||
|
||||
_initStyle(ctx) {
|
||||
// @ts-ignore
|
||||
ctx.createRadialGradient = () => {
|
||||
return ctx.createCircularGradient(arguments);
|
||||
};
|
||||
}
|
||||
|
||||
_initEvent() {
|
||||
this.event = {};
|
||||
const eventNames = [{
|
||||
wxName: 'touchStart',
|
||||
ecName: 'mousedown'
|
||||
}, {
|
||||
wxName: 'touchMove',
|
||||
ecName: 'mousemove'
|
||||
}, {
|
||||
wxName: 'touchEnd',
|
||||
ecName: 'mouseup'
|
||||
}, {
|
||||
wxName: 'touchEnd',
|
||||
ecName: 'click'
|
||||
}];
|
||||
|
||||
eventNames.forEach(name => {
|
||||
this.event[name.wxName] = e => {
|
||||
const touch = e.touches[0];
|
||||
this.chart.getZr().handler.dispatch(name.ecName, {
|
||||
zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
|
||||
zrY: name.wxName === 'tap' ? touch.clientY : touch.y
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
addEventListener(el, name, handler, opt) {
|
||||
// el?.addEventListener(name, handler, opt);
|
||||
}
|
||||
set width(w) {
|
||||
if (this.canvasNode) this.canvasNode.width = w
|
||||
}
|
||||
set height(h) {
|
||||
if (this.canvasNode) this.canvasNode.height = h
|
||||
}
|
||||
|
||||
get width() {
|
||||
if (this.canvasNode)
|
||||
return this.canvasNode.width
|
||||
return 0
|
||||
}
|
||||
get height() {
|
||||
if (this.canvasNode)
|
||||
return this.canvasNode.height
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { use } from 'echarts/lib/extension.js';
|
||||
export * from 'echarts/lib/export/core.js';
|
||||
import { install as CanvasRenderer } from 'echarts/lib/renderer/installCanvasRenderer.js';
|
||||
import { install as LineChart } from 'echarts/lib/chart/line/install.js';
|
||||
import { install as BarChart } from 'echarts/lib/chart/bar/install.js';
|
||||
import { install as PieChart } from 'echarts/lib/chart/pie/install.js';
|
||||
import { install as GridSimpleComponent } from 'echarts/lib/component/grid/installSimple.js';
|
||||
import { install as AriaComponent } from 'echarts/lib/component/aria/install.js';
|
||||
import { install as DatasetComponent } from 'echarts/lib/component/dataset/install.js';
|
||||
use([CanvasRenderer]);
|
||||
use([LineChart, BarChart, PieChart]);
|
||||
use([GridSimpleComponent, AriaComponent, DatasetComponent]);
|
||||
@@ -0,0 +1,397 @@
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { computed, ref, onMounted, watch, PropType, getCurrentInstance, nextTick } from "vue";
|
||||
import { arrayNumberValid, getUid, arrayNumberValidByStyleMP, arrayNumberValidByStyleBorderColor, arrayNumberValidByStyleBorderStyle, covetUniNumber, linearValid, getUnit } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import WxCanvas from './canvasinit';
|
||||
// @ts-ignore
|
||||
import * as echarts from "./simple.js";
|
||||
import { ECharts } from "echarts";
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
let canvasId = ref("canvasId")
|
||||
// #ifdef H5 || APP-PLUS || APP-VUE
|
||||
canvasId.value = "tm" + new Date().getTime().toString();
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* @displayName 百度图表
|
||||
* @exportName tm-echart
|
||||
* @category 展示组件
|
||||
* @description 可以通过onInt或者ref函数getChart取得ECharts对象自行处理和设置数据,这个组件使用会超过530kb+大小,请小心你的包大小限制,超过大小哦.
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmF2' });
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 初始成功触发
|
||||
* @param {Cart} chart对象
|
||||
*/
|
||||
'onInit',
|
||||
/**
|
||||
* 图表元素被点击时触发
|
||||
* @param {Object} chart的事件对象参数
|
||||
*/
|
||||
'click'
|
||||
])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 宽度css合法值,不要用auto
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '100%'
|
||||
},
|
||||
/**
|
||||
* 高度,css合法值,不要用auto,需要一个值,单位随意,不带单位默认是rpx
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: '300px'
|
||||
}
|
||||
})
|
||||
|
||||
const _width = computed(() => covetUniNumber(props.width, config.unit))
|
||||
const _height = computed(() => covetUniNumber(props.height, config.unit))
|
||||
let ctx: UniApp.CanvasContext;
|
||||
let ctxNode: any;
|
||||
let chart: echarts.ECharts | null = null
|
||||
const pixelRatio = uni.getSystemInfoSync().pixelRatio
|
||||
const is2d = ref(false)
|
||||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-QQ
|
||||
is2d.value = true
|
||||
// #endif
|
||||
const isPc = ref(false)
|
||||
|
||||
isPc.value = uni.getSystemInfoSync().deviceType == 'pc' ? true : false;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (is2d.value) {
|
||||
setTimeout(() => MpWeix_init(), 100)
|
||||
} else {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
setTimeout(() => appvueH5Other(), 50)
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
//appvue,h5,和其它平台。
|
||||
async function appvueH5Other(fun: Function) {
|
||||
const { width, height } = await getDomSize()
|
||||
echarts.registerPreprocessor((option: any) => {
|
||||
if (option && option.series) {
|
||||
if (option.series.length > 0) {
|
||||
option.series.forEach((series: echarts.SeriesOption) => {
|
||||
series.progressive = 0;
|
||||
});
|
||||
} else if (typeof option.series === 'object') {
|
||||
option.series.progressive = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
ctx = uni.createCanvasContext(canvasId.value, proxy);
|
||||
|
||||
if (!isPc.value) {
|
||||
const canvas: any = new WxCanvas(ctx, canvasId.value, false, false)
|
||||
echarts.setPlatformAPI({ createCanvas: () => canvas });
|
||||
chart = echarts.init(canvas, "", {
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
canvas.setChart(chart);
|
||||
} else {
|
||||
const canvasNode: HTMLCanvasElement | undefined = document.querySelector('#' + canvasId.value)?.getElementsByTagName("canvas")[0];
|
||||
document.querySelector('#' + canvasId.value)?.removeChild(document.querySelector('#' + canvasId.value)?.getElementsByTagName("div")[0])
|
||||
ctx = canvasNode?.getContext("2d")
|
||||
const canvas: any = new WxCanvas(ctx, canvasId.value, false, false)
|
||||
chart = echarts.init(canvasNode);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
}
|
||||
|
||||
emits("onInit", chart)
|
||||
if (typeof fun === 'function') {
|
||||
fun(chart)
|
||||
}
|
||||
}
|
||||
//支付宝和微信,QQ 支持2d渲染。
|
||||
async function MpWeix_init(fun: Function) {
|
||||
const { width, height } = await getDomSize()
|
||||
|
||||
echarts.registerPreprocessor((option: any) => {
|
||||
if (option && option.series) {
|
||||
if (option.series.length > 0) {
|
||||
option.series.forEach((series: echarts.SeriesOption) => {
|
||||
series.progressive = 0;
|
||||
});
|
||||
} else if (typeof option.series === 'object') {
|
||||
option.series.progressive = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const query = uni.createSelectorQuery().in(proxy)
|
||||
// #ifdef MP-ALIPAY
|
||||
query.select('#canvasId').node().exec((res2) => {
|
||||
const canvasNode = res2[0].node;
|
||||
|
||||
let ctxvb: UniApp.CanvasContext = canvas.getContext('2d');
|
||||
canvasNode.width = res[0].width * pixelRatio
|
||||
canvasNode.height = res[0].height * pixelRatio
|
||||
ctx = ctxvb;
|
||||
ctxNode = canvasNode
|
||||
const canvas = new WxCanvas(ctx, canvasId.value, true, canvasNode)
|
||||
echarts.setPlatformAPI({
|
||||
// Same with the old setCanvasCreator
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
},
|
||||
});
|
||||
chart = echarts.init(canvas, null, {
|
||||
width: width,
|
||||
height: height,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
canvas.setChart(chart);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
emits("onInit", chart)
|
||||
if (typeof fun === 'function') {
|
||||
fun(chart)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-QQ
|
||||
query.select('#canvasId')
|
||||
.fields({
|
||||
node: true,
|
||||
size: true,
|
||||
context: true
|
||||
})
|
||||
.exec((res) => {
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const canvasNode = res[0].node
|
||||
const ctxvb = canvasNode.getContext('2d')
|
||||
canvasNode.width = res[0].width * pixelRatio
|
||||
canvasNode.height = res[0].height * pixelRatio
|
||||
ctxvb.scale(pixelRatio, pixelRatio)
|
||||
ctx = ctxvb;
|
||||
ctxNode = canvasNode
|
||||
const canvas = new WxCanvas(ctx, canvasId.value, true, canvasNode)
|
||||
echarts.setPlatformAPI({
|
||||
// Same with the old setCanvasCreator
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
},
|
||||
});
|
||||
|
||||
chart = echarts.init(canvas, null, {
|
||||
width: width,
|
||||
height: height,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
|
||||
canvas.setChart(chart);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
emits("onInit", chart)
|
||||
|
||||
// #endif
|
||||
// #ifdef MP-QQ
|
||||
|
||||
const canvasNode = {}
|
||||
const ctxvb = res[0].context
|
||||
canvasNode.width = res[0].width * pixelRatio
|
||||
canvasNode.height = res[0].height * pixelRatio
|
||||
ctxvb.scale(pixelRatio, pixelRatio)
|
||||
ctx = ctxvb;
|
||||
const canvas = new WxCanvas(ctx, canvasId.value, true, canvasNode)
|
||||
echarts.setPlatformAPI({
|
||||
// Same with the old setCanvasCreator
|
||||
createCanvas() {
|
||||
return canvas;
|
||||
},
|
||||
});
|
||||
chart = echarts.init(canvas, null, {
|
||||
width: width,
|
||||
height: height,
|
||||
devicePixelRatio: pixelRatio
|
||||
});
|
||||
canvas.setChart(chart);
|
||||
chart.on('click', (e) => {
|
||||
emits('click', e)
|
||||
})
|
||||
emits("onInit", chart)
|
||||
// #endif
|
||||
|
||||
if (typeof fun === 'function') {
|
||||
fun(chart)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
function getChart(): Promise<ECharts> {
|
||||
return new Promise((res, rej) => {
|
||||
if (is2d.value) {
|
||||
setTimeout(() => MpWeix_init(chart => res(chart)), 100)
|
||||
} else {
|
||||
setTimeout(() => appvueH5Other(chart => res(chart)), 50)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getImg() {
|
||||
return new Promise((res, rej) => {
|
||||
if (!ctx) {
|
||||
console.error('tmChart没有初始化')
|
||||
rej('没有初始化')
|
||||
return
|
||||
}
|
||||
uni.canvasToTempFilePath({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: _width.value,
|
||||
height: _height.value,
|
||||
canvasId: canvasId.value,
|
||||
canvas: ctxNode,
|
||||
success: function (respone) {
|
||||
// 在H5平台下,tempFilePath 为 base64
|
||||
res(respone.tempFilePath);
|
||||
},
|
||||
fail: (respone) => {
|
||||
console.error(respone)
|
||||
rej(res);
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getDomSize = (): Promise<{ width: number, height: number }> => {
|
||||
return new Promise((res, rej) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('.canvas')
|
||||
.boundingClientRect()
|
||||
.exec((result) => {
|
||||
let node = result[0]
|
||||
res({
|
||||
width: node.width,
|
||||
height: node.height
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
defineExpose({ getChart, getImg })
|
||||
|
||||
|
||||
function touchStart(e: TouchEvent) {
|
||||
if (chart && e.touches.length > 0) {
|
||||
var touch = e.touches[0];
|
||||
var handler = chart.getZr().handler;
|
||||
handler.dispatch('mousedown', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.dispatch('mousemove', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'start');
|
||||
// emits('touchStart', e)
|
||||
}
|
||||
}
|
||||
|
||||
function touchMove(e: TouchEvent) {
|
||||
if (chart && e.touches.length > 0) {
|
||||
var touch = e.touches[0];
|
||||
var handler = chart.getZr().handler;
|
||||
handler.dispatch('mousemove', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'change');
|
||||
// emits('touchMove', e)
|
||||
}
|
||||
}
|
||||
|
||||
function touchEnd(e: TouchEvent) {
|
||||
if (chart) {
|
||||
const touch = e.changedTouches ? e.changedTouches[0] : {};
|
||||
var handler = chart.getZr().handler;
|
||||
handler.dispatch('mouseup', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y,
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { }
|
||||
});
|
||||
handler.dispatch('click', {
|
||||
zrX: touch.x,
|
||||
zrY: touch.y
|
||||
});
|
||||
handler.processGesture(wrapTouch(e), 'end');
|
||||
// emits('touchEnd', e)
|
||||
}
|
||||
}
|
||||
|
||||
function wrapTouch(event: TouchEvent) {
|
||||
for (let i = 0; i < event.touches.length; ++i) {
|
||||
const touch = event.touches[i];
|
||||
touch.offsetX = touch.x;
|
||||
touch.offsetY = touch.y;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="css" scoped></style>
|
||||
<template>
|
||||
|
||||
<!-- #ifndef MP-WEIXIN || MP-ALIPAY || MP-QQ || APP-NVUE -->
|
||||
<canvas v-if="!isPc" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" :id="canvasId"
|
||||
:canvas-id="canvasId" class="canvas" :style="{ width: _width, height: _height }"></canvas>
|
||||
|
||||
<canvas v-if="isPc" :id="canvasId" :canvas-id="canvasId" class="canvas"
|
||||
:style="{ width: _width, height: _height }"></canvas>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||||
<canvas @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" type="2d" id="canvasId"
|
||||
canvas-id="canvasId" class="canvas" :style="{ width: _width, height: _height }"></canvas>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<canvas @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" type="2d" :id="canvasId"
|
||||
:canvas-id="canvasId" class="canvas" :style="{ width: _width, height: _height }"></canvas>
|
||||
<!-- #endif -->
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,262 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance, inject, onBeforeUnmount, watch, type PropType, onMounted } from 'vue';
|
||||
import { defaultValidator } from "./../tm-form/validator"
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import TmForm from '../tm-form/tm-form.vue';
|
||||
import { covetUniNumber } from '../../libs/tool';
|
||||
import { getDefaultColor } from '../../libs/colors';
|
||||
|
||||
/**
|
||||
* @displayName 表单子组件
|
||||
* @exportName tm-form-item
|
||||
* @category 表单组件
|
||||
* @page tm-form
|
||||
* @description 表单组件的校验规则现在统一放到了form组件上,form-item上不再配置校验,主要是方便统一管理校验模块,并且校验函数作了升级处理。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmFormItem" });
|
||||
const { config } = useTmConfig();
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 表单字段唯一标识。
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* 必款时的符号,如果为false,或者空则不显示
|
||||
*/
|
||||
required: {
|
||||
type: [String, Boolean],
|
||||
default: "*"
|
||||
},
|
||||
/**
|
||||
* 表单标题
|
||||
*/
|
||||
label: {
|
||||
type: [String],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 方向,这里会覆盖form父上的方向值。
|
||||
*/
|
||||
direction: {
|
||||
type: String as PropType<"horizontal" | "vertical">,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 方向,里会覆盖form父上的值。
|
||||
*/
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 标题字号大小,会覆盖父form值
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 标题颜色,会覆盖父form值
|
||||
*/
|
||||
labelFontColor: {
|
||||
type: [String],
|
||||
default: ""
|
||||
},
|
||||
})
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
let tid = 0;
|
||||
const isPass = ref<boolean | null>(null)
|
||||
const errorMessage = ref<string | null>(null)
|
||||
|
||||
const tmFormModelValue = inject("tmFormModelValue", computed((): Record<string, any> => {
|
||||
return {}
|
||||
}))
|
||||
const tmFormVaildType = inject("tmFormVaildType", computed(() => ''))
|
||||
const tmFormDirection = inject("tmFormDirection", computed(() => ''))
|
||||
const tmFormLabelwidth = inject("tmFormLabelwidth", computed(() => ''))
|
||||
const tmFormGap = inject("tmFormGap", computed(() => '0'))
|
||||
const tmFormLabelFontSize = inject("tmFormLabelFontSize", computed(() => '30'))
|
||||
const tmFormLabelFontColor = inject("tmFormLabelFontColor", computed(() => '#333333'))
|
||||
const _tmFormGap = computed(() => covetUniNumber(tmFormGap.value))
|
||||
const _tmFormDirection = computed(() => {
|
||||
if (props.direction) return props.direction
|
||||
if (tmFormDirection.value) return tmFormDirection.value
|
||||
return "horizontal"
|
||||
})
|
||||
const _labelWidth = computed(() => {
|
||||
if (props.labelWidth) return covetUniNumber(props.labelWidth)
|
||||
if (tmFormLabelwidth.value) return covetUniNumber(tmFormLabelwidth.value)
|
||||
return "190rpx"
|
||||
})
|
||||
const _tmFormLabelFontSize = computed(() => {
|
||||
if (props.labelFontSize) return covetUniNumber(props.labelFontSize)
|
||||
if (tmFormLabelFontSize.value) return covetUniNumber(tmFormLabelFontSize.value)
|
||||
return "30rpx"
|
||||
})
|
||||
const _tmFormLabelFontColor = computed(() => {
|
||||
if (props.labelFontColor) return getDefaultColor(props.labelFontColor)
|
||||
if (tmFormLabelFontColor.value) return getDefaultColor(tmFormLabelFontColor.value)
|
||||
return "#333333"
|
||||
})
|
||||
|
||||
const _required = computed(() => props.required)
|
||||
const _label = computed(() => props.label)
|
||||
const _showLabel = computed(() => props.showLabel)
|
||||
const _showBottom = computed(() => props.showBottom)
|
||||
const isFirstVaild = ref(true)
|
||||
watch(() => tmFormModelValue.value[props.name], (val: any) => {
|
||||
if (tmFormVaildType.value != 'valid') return
|
||||
if (isFirstVaild.value) {
|
||||
isFirstVaild.value = false;
|
||||
return
|
||||
}
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(() => {
|
||||
_validFun(val)
|
||||
}, 200);
|
||||
}, { deep: true })
|
||||
|
||||
watch(tmFormVaildType, (val: string) => {
|
||||
if (val == 'reset' || val == '') {
|
||||
isPass.value = null;
|
||||
errorMessage.value = null;
|
||||
}
|
||||
if (val == 'valid') {
|
||||
let value = tmFormModelValue.value[props.name]
|
||||
if (typeof value !== 'undefined') {
|
||||
_validFun(value)
|
||||
}
|
||||
isFirstVaild.value = false;
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const _validFun = (val: any) => {
|
||||
if (!proxy) return;
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) return;
|
||||
let ele = parent! as InstanceType<typeof TmForm>
|
||||
let valid = ele._validate(props.name, val)
|
||||
if (typeof valid == 'boolean') {
|
||||
if (valid === true) {
|
||||
isPass.value = true;
|
||||
errorMessage.value = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!valid.validate) {
|
||||
isPass.value = false;
|
||||
errorMessage.value = valid.message;
|
||||
} else {
|
||||
isPass.value = true;
|
||||
errorMessage.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const setVisibled = (isvisible = true) => {
|
||||
if (!proxy) return;
|
||||
let parent = findParent(proxy)
|
||||
if (!parent) return;
|
||||
let ele = parent! as InstanceType<typeof TmForm>
|
||||
ele._setMarker(props.name, isvisible)
|
||||
}
|
||||
function findParent(parent: any): any {
|
||||
if (parent == null) return null;
|
||||
if (parent.$parent?.$options?.name == "TmForm") return parent.$parent;
|
||||
let parents = findParent(parent.$parent);
|
||||
if (parents?.$options?.name == "TmForm") return parents;
|
||||
return null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setVisibled(true)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid)
|
||||
setVisibled(false)
|
||||
})
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmFormItem" :style="{ marginBottom: _tmFormGap }">
|
||||
<view class="tmFormItemWrap"
|
||||
:class="[_tmFormDirection == 'horizontal' ? 'tmFormItemWrapRow' : 'tmFormItemWrapCol']">
|
||||
|
||||
<view v-if="_showLabel" class="tmFormItemLabel"
|
||||
:style="{ width: _tmFormDirection == 'horizontal' ? _labelWidth : 'auto', fontSize: _tmFormLabelFontSize, color: _tmFormLabelFontColor }">
|
||||
<text class="tmFormRequired" v-if="_required">{{ _required }}</text>
|
||||
<slot name="label">
|
||||
{{ _label }}
|
||||
</slot>
|
||||
</view>
|
||||
<view class="tmFormItemWrapContent">
|
||||
<!--
|
||||
@slot 表单默认插槽,可以不是直接form-item直接子节点,可以根据自己需要随意布局,只要form-item在里面即可.
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tmFormError" v-if="errorMessage && !isPass">
|
||||
{{ errorMessage }}
|
||||
</view>
|
||||
<view v-if="_showBottom" :style="{ marginTop: _tmFormGap, height: '1px', backgroundColor: '#f5f5f5' }"></view>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tmFormItemWrapRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tmFormItemLabel {
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.tmFormItemWrapContent {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tmFormRequired {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tmFormError {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import { type PropType, computed, nextTick, provide, ref } from 'vue';
|
||||
import { defaultValidator } from "./validator"
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { deepClone } from '../../libs/tool';
|
||||
|
||||
/**
|
||||
* @displayName 表单
|
||||
* @exportName tm-form
|
||||
* @category 表单组件
|
||||
* @description 表单组件的校验规则现在统一放到了form组件上,form-item上不再配置校验,主要是方便统一管理校验模块,并且校验函数作了升级处理。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmForm" });
|
||||
const { config } = useTmConfig();
|
||||
const emit = defineEmits([
|
||||
/**
|
||||
* 提交表单时触发.
|
||||
* @param {TM.FORM_SUBMIT_RESULT} result 校验结果
|
||||
*/
|
||||
"submit",
|
||||
/**
|
||||
* 表单数据时一定要vmodel绑定,当重置时会触发此事件。
|
||||
*/
|
||||
"update:modelValue"
|
||||
])
|
||||
const props = defineProps({
|
||||
// 表单数据
|
||||
modelValue: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 表单校验规则
|
||||
* 经过验证后,觉得把form-item的rules属性去掉,直接使用form上统一到一起rules属性,这样就可以不用写form-item了.
|
||||
*/
|
||||
rules: {
|
||||
type: Object as PropType<Record<string, Array<TM.FORM_RULE> | TM.FORM_RULE>>,
|
||||
default: (): Record<string, Array<TM.FORM_RULE>> => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 方向,子组件的方向会覆盖此处。
|
||||
*/
|
||||
direction: {
|
||||
type: String as PropType<"horizontal" | "vertical">,
|
||||
default: "horizontal"
|
||||
},
|
||||
/**
|
||||
* 方向,子组件的覆盖此处。
|
||||
*/
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: "160"
|
||||
},
|
||||
/**
|
||||
* 标题字号大小
|
||||
*/
|
||||
labelFontSize: {
|
||||
type: [String, Number],
|
||||
default: "30"
|
||||
},
|
||||
/**
|
||||
* 标题颜色
|
||||
*/
|
||||
labelFontColor: {
|
||||
type: [String],
|
||||
default: "#333333"
|
||||
},
|
||||
/**
|
||||
* 间隙,项目之间的间距。
|
||||
*/
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
default: "10"
|
||||
}
|
||||
})
|
||||
const ruleCover = (rule: TM.FORM_RULE): TM.FORM_RULE_TYPE => {
|
||||
return {
|
||||
required: rule?.required ?? false,
|
||||
message: rule?.message ?? "请正确认选择/填写",
|
||||
validator: rule?.validator ?? null,
|
||||
min: rule?.min ?? 1,
|
||||
max: rule?.max ?? -1,
|
||||
type: rule?.type ?? 'auto',
|
||||
rule: rule?.rule ?? null
|
||||
}
|
||||
}
|
||||
const _rules = computed((): Record<string, Array<TM.FORM_RULE_TYPE>> => {
|
||||
let rules: Record<string, Array<TM.FORM_RULE_TYPE>> = {};
|
||||
let keys = Object.keys(props.modelValue)
|
||||
for (let key in props.rules) {
|
||||
if (keys.includes(key)) {
|
||||
let item = props.rules[key];
|
||||
if (Array.isArray(item)) {
|
||||
rules[key] = item.map(item => ruleCover(item))
|
||||
} else if (typeof props.rules[key] == 'object' && !Array.isArray(props.rules[key])) {
|
||||
rules[key] = [ruleCover(item)]
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules
|
||||
})
|
||||
const vaildTypeStr = ref('valid')
|
||||
provide("tmFormModelValue", computed(() => props.modelValue))
|
||||
provide("tmFormVaildType", computed(() => vaildTypeStr.value))
|
||||
provide("tmFormDirection", computed(() => props.direction))
|
||||
provide("tmFormLabelwidth", computed(() => props.labelWidth))
|
||||
provide("tmFormGap", computed(() => props.gap))
|
||||
provide("tmFormLabelFontSize", computed(() => props.labelFontSize))
|
||||
provide("tmFormLabelFontColor", computed(() => props.labelFontColor))
|
||||
const oldbackValue = deepClone(props.modelValue)
|
||||
// 被标记的字段
|
||||
const markedKeys: string[] = []
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const submit = () => {
|
||||
vaildTypeStr.value = ''
|
||||
nextTick(() => {
|
||||
vaildTypeStr.value = 'valid'
|
||||
})
|
||||
emit("submit", validate())
|
||||
}
|
||||
/**
|
||||
* 获取校验结果,不会校验页面.
|
||||
* 如果想触发submit事件,并校验页面提示,请使用ref函数submit()而不是此函数方法.
|
||||
*/
|
||||
const validate = (): TM.FORM_SUBMIT_RESULT => {
|
||||
let result: TM.FORM_SUBMIT_RESULT = {
|
||||
//整体是否校验通过
|
||||
isPass: true,
|
||||
//第一个校验不通过的字段
|
||||
firstValid: null,
|
||||
//校验结果
|
||||
result: []
|
||||
}
|
||||
for (let key of markedKeys) {
|
||||
let rules = _rules.value[key]
|
||||
if (!rules) continue
|
||||
for (let rule of rules) {
|
||||
if (!defaultValidator(props.modelValue[key], rule)) {
|
||||
result.isPass = false;
|
||||
if (result.firstValid == null) {
|
||||
result.firstValid = { key, isPass: false, message: rule.message, data: props.modelValue[key] }
|
||||
}
|
||||
result.result.push({ key, isPass: false, message: rule.message, data: props.modelValue[key] })
|
||||
} else {
|
||||
result.result.push({ key, isPass: true, message: '校验通过', data: props.modelValue[key] })
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 重置表单,数据恢复至最开始的数据.
|
||||
*/
|
||||
const reset = () => {
|
||||
vaildTypeStr.value = 'reset'
|
||||
emit('update:modelValue', deepClone(oldbackValue))
|
||||
}
|
||||
/**
|
||||
* 单个校验,子组件用。(外部请不要使用.)
|
||||
*/
|
||||
const _validate = (name: string, value: any): { validate: boolean, message: string } | boolean => {
|
||||
let rules = _rules.value[name]
|
||||
if (!rules) return true
|
||||
for (let rule of rules) {
|
||||
if (!defaultValidator(value, rule)) {
|
||||
return { validate: false, message: rule.message }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const _setMarker = (name: string, isvisibl: boolean) => {
|
||||
let index = markedKeys.findIndex((el) => el == name);
|
||||
if (isvisibl) {
|
||||
if (index == -1) {
|
||||
markedKeys.push(name)
|
||||
}
|
||||
} else {
|
||||
if (index > -1) {
|
||||
markedKeys.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
submit,
|
||||
validate,
|
||||
reset,
|
||||
_validate,
|
||||
_setMarker
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<view class="tmForm">
|
||||
<!--
|
||||
@slot 表单默认插槽,可以不是直接form-item直接子节点,可以根据自己需要随意布局,只要form-item在里面即可.
|
||||
-->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,82 @@
|
||||
//指定了类型校验
|
||||
const defaultValidatorByType = (value: any, rule: TM.FORM_RULE_TYPE) => {
|
||||
|
||||
//但也有可能是string
|
||||
if (rule.type == 'number') {
|
||||
let val = typeof value == 'string' ? parseFloat(value) : value
|
||||
if (isNaN(value)) return false;
|
||||
if (rule.max == -1) return value >= rule.min
|
||||
return value >= rule.min && value <= rule.max
|
||||
}
|
||||
if (typeof value == 'undefined' || typeof value == null) {
|
||||
return false
|
||||
}
|
||||
if (rule.type == 'string') {
|
||||
let val = value.trim()
|
||||
if (val === '') return false;
|
||||
let len = value.split('').length
|
||||
if (rule.max == -1) return len >= rule.min
|
||||
return len >= rule.min && len <= rule.max
|
||||
}
|
||||
if (Array.isArray(value) && rule.type == 'array') {
|
||||
if (value.length == 0) return false;
|
||||
if (rule.max == -1) return value.length >= rule.min
|
||||
return value.length >= rule.min && value.length <= rule.max
|
||||
}
|
||||
|
||||
if (rule.type == 'date') {
|
||||
if (typeof value == 'string') {
|
||||
let date = new Date(value.replace(/-/g, '/')).getTime()
|
||||
return !isNaN(date)
|
||||
} else if (typeof value == 'number') {
|
||||
let date = new Date(value).getTime()
|
||||
return !isNaN(date)
|
||||
}
|
||||
}
|
||||
if (rule.type == 'phone') {
|
||||
let val = typeof value == 'string' ? value : value.toString()
|
||||
let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
|
||||
return val.match(reg);
|
||||
}
|
||||
if (rule.type == 'email' && typeof value == 'string') {
|
||||
let reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
export const defaultValidator = (value: any, rule: TM.FORM_RULE_TYPE) => {
|
||||
if (!rule.required) return true;
|
||||
if(rule.validator) return rule.validator(value);
|
||||
if(rule.rule) {
|
||||
return rule.rule.test(value);
|
||||
}
|
||||
if (rule.type != 'auto') return defaultValidatorByType(value, rule)
|
||||
if (typeof value == 'number') {
|
||||
if (isNaN(value)) return false;
|
||||
if (rule.max == -1) return value >= rule.min
|
||||
return value >= rule.min && value <= rule.max
|
||||
}
|
||||
if (typeof value == 'undefined' || typeof value == null) {
|
||||
return false
|
||||
}
|
||||
if (typeof value == 'string') {
|
||||
let val = value.trim()
|
||||
if (val === '') return false;
|
||||
let len = value.split('').length
|
||||
if (rule.max == -1) return len >= rule.min
|
||||
return len >= rule.min && len <= rule.max
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length == 0) return false;
|
||||
if (rule.max == -1) return value.length >= rule.min
|
||||
return value.length >= rule.min && value.length <= rule.max
|
||||
}
|
||||
|
||||
if (typeof value == 'object') {
|
||||
let keys = Object.keys(value)
|
||||
return keys.length > 0
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
<template>
|
||||
<view :id="attrs.id" :class="'_block _'+name+' '+attrs.class" :style="attrs.style">
|
||||
<block v-for="(n, i) in childs" v-bind:key="i">
|
||||
<!-- 图片 -->
|
||||
<!-- 占位图 -->
|
||||
<image v-if="n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)&&typeof (ctrl[i]<0?opts[2]:opts[1]) == 'string'" class="_img" :style="n.attrs.style"
|
||||
:src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
|
||||
<!-- 显示图片 -->
|
||||
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
|
||||
<img v-if="n.name==='img'&&typeof (n.attrs.src||(ctrl.load?n.attrs['data-src']:'')) == 'string'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style"
|
||||
:src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
|
||||
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
|
||||
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t"
|
||||
:nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i"
|
||||
@tap.stop="imgTap(n.attrs.sr)" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style"
|
||||
:src="n.attrs.src" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :lazy-load="opts[0]"
|
||||
:webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore"
|
||||
:image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src)" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-KUAISHOU -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src" :lazy-load="opts[0]"
|
||||
:data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap(n.attrs.src)"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS && VUE3 -->
|
||||
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class"
|
||||
:style="(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style"
|
||||
:src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')"
|
||||
:mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||''))" :data-i="i" @load="imgLoad" @error="mediaError"
|
||||
@tap.stop="imgTap(n.attrs.src||(ctrl.load?n.attrs['data-src']:''))" @longpress="imgLongTap" />
|
||||
<!-- #endif -->
|
||||
<!-- 文本 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text v-else-if="n.text" :user-select="opts[4]=='force'&&isiOS" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
|
||||
<text v-else-if="n.text" decode>{{n.text}}</text>
|
||||
<!-- #endif -->
|
||||
<text v-else-if="n.name==='br'">\n</text>
|
||||
<!-- 链接 -->
|
||||
<view v-else-if="n.name==='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class"
|
||||
hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
|
||||
<node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
|
||||
</view>
|
||||
<!-- 视频 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style"
|
||||
v-html="n.html" @vplay.stop="play" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<video v-else-if="n.name==='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style"
|
||||
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted"
|
||||
:object-fit="n.attrs['object-fit']" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i"
|
||||
@play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 || APP-PLUS -->
|
||||
<iframe v-else-if="n.name==='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen"
|
||||
:frameborder="n.attrs.frameborder" :src="n.attrs.src" />
|
||||
<embed v-else-if="n.name==='embed'" :style="n.attrs.style" :src="n.attrs.src" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
|
||||
<!-- 音频 -->
|
||||
<audio v-else-if="n.name==='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style"
|
||||
:author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name"
|
||||
:poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
|
||||
<!-- #endif -->
|
||||
<view v-else-if="(n.name==='table'&&n.c)||n.name==='li'" :id="n.attrs.id"
|
||||
:class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
|
||||
<node v-if="n.name==='li'" :childs="n.children" :opts="opts" />
|
||||
<view v-else v-for="(tbody, x) in n.children" v-bind:key="x"
|
||||
:class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
|
||||
<node v-if="tbody.name==='td'||tbody.name==='th'" :childs="tbody.children" :opts="opts" />
|
||||
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
|
||||
<view v-if="tr.name==='td'||tr.name==='th'" :class="'_'+tr.name+' '+tr.attrs.class"
|
||||
:style="tr.attrs.style">
|
||||
<node :childs="tr.children" :opts="opts" />
|
||||
</view>
|
||||
<view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
|
||||
<view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class"
|
||||
:style="td.attrs.style">
|
||||
<node :childs="td.children" :opts="opts" />
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 富文本 -->
|
||||
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c&&!handler.isInline(n.name, n.attrs.style)" :id="n.attrs.id" :style="n.f"
|
||||
:user-select="opts[4]" :nodes="[n]" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
|
||||
<rich-text v-else-if="!n.c" :id="n.attrs.id" :style="'display:inline;'+n.f" :preview="false"
|
||||
:selectable="opts[4]" :user-select="opts[4]" :nodes="[n]" />
|
||||
<!-- #endif -->
|
||||
<!-- 继续递归 -->
|
||||
<view v-else-if="n.c===2" :id="n.attrs.id" :class="'_block _'+n.name+' '+n.attrs.class"
|
||||
:style="n.f+';'+n.attrs.style">
|
||||
<node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs"
|
||||
:childs="n2.children" :opts="opts" />
|
||||
</view>
|
||||
<node :imgList="imgs" @imgtap="onimgTop" v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
<script module="handler" lang="wxs">
|
||||
// 行内标签列表
|
||||
var inlineTags = {
|
||||
abbr: true,
|
||||
b: true,
|
||||
big: true,
|
||||
code: true,
|
||||
del: true,
|
||||
em: true,
|
||||
i: true,
|
||||
ins: true,
|
||||
label: true,
|
||||
q: true,
|
||||
small: true,
|
||||
span: true,
|
||||
strong: true,
|
||||
sub: true,
|
||||
sup: true
|
||||
}
|
||||
/**
|
||||
* @description 判断是否为行内标签
|
||||
*/
|
||||
module.exports = {
|
||||
isInline: function(tagName, style) {
|
||||
return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
import node from './mp-node.vue'
|
||||
export default {
|
||||
name: 'node',
|
||||
options: {
|
||||
// #ifdef MP-WEIXIN
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
addGlobalClass: false
|
||||
// #endif
|
||||
},
|
||||
emits:['imgtap'],
|
||||
data() {
|
||||
return {
|
||||
ctrl: {},
|
||||
imgs:[],
|
||||
// #ifdef MP-WEIXIN
|
||||
isiOS: uni.getSystemInfoSync().system.includes('iOS')
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
attrs: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
childs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
opts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
imgList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
watch:{
|
||||
imgs:{
|
||||
handler(val){
|
||||
this.imgs = val;
|
||||
},
|
||||
deep:true,
|
||||
immediate:true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
||||
// #ifndef (H5 || APP-PLUS) && VUE3
|
||||
node
|
||||
// #endif
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (this.observer) {
|
||||
this.observer.disconnect()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// #ifdef MP-WEIXIN
|
||||
toJSON() {
|
||||
return this
|
||||
},
|
||||
// #endif
|
||||
/**
|
||||
* @description 播放视频事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
play(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
this.root.$emit('play', {
|
||||
source: node.name,
|
||||
attrs: {
|
||||
...node.attrs,
|
||||
src: node.src[this.ctrl[i] || 0]
|
||||
}
|
||||
})
|
||||
// #ifndef APP-PLUS
|
||||
if (this.root.pauseVideo) {
|
||||
let flag = false
|
||||
const id = e.target.id
|
||||
for (let i = this.root._videos.length; i--;) {
|
||||
if (this.root._videos[i].id === id) {
|
||||
flag = true
|
||||
} else {
|
||||
this.root._videos[i].pause() // 自动暂停其他视频
|
||||
}
|
||||
}
|
||||
// 将自己加入列表
|
||||
if (!flag) {
|
||||
const ctx = uni.createVideoContext(id
|
||||
// #ifndef MP-BAIDU
|
||||
, this
|
||||
// #endif
|
||||
)
|
||||
ctx.id = id
|
||||
if (this.root.playbackRate) {
|
||||
ctx.playbackRate(this.root.playbackRate)
|
||||
}
|
||||
this.root._videos.push(ctx)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgTap(e) {
|
||||
|
||||
this.$emit('imgtap', e)
|
||||
// 自动预览图片
|
||||
if (this.opts[5]) {
|
||||
uni.previewImage({
|
||||
// #ifdef MP-WEIXIN
|
||||
showmenu: this.opts[3],
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
enablesavephoto: this.opts[3],
|
||||
enableShowPhotoDownload:this.opts[3],
|
||||
// #endif
|
||||
current:0,
|
||||
urls: [e]
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onimgTop(src){
|
||||
this.$emit('imgtap', src)
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片长按
|
||||
*/
|
||||
imgLongTap(e) {
|
||||
// #ifdef APP-PLUS
|
||||
const attrs = this.childs[e.currentTarget.dataset.i].attrs
|
||||
if (this.opts[3] && !attrs.ignore) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['保存图片'],
|
||||
success: () => {
|
||||
const save = path => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path,
|
||||
success() {
|
||||
uni.showToast({
|
||||
title: '保存成功'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.root.imgList[attrs.i].startsWith('http')) {
|
||||
uni.downloadFile({
|
||||
url: this.root.imgList[attrs.i],
|
||||
success: res => save(res.tempFilePath)
|
||||
})
|
||||
} else {
|
||||
save(this.root.imgList[attrs.i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 图片加载完成事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
imgLoad(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
/* #ifndef H5 || (APP-PLUS && VUE2) */
|
||||
if (!this.childs[i].w) {
|
||||
// 设置原宽度
|
||||
this.$set(this.ctrl, i, e.detail.width)
|
||||
} else /* #endif */
|
||||
if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
|
||||
// 加载完毕,取消加载中占位图
|
||||
this.$set(this.ctrl, i, 1)
|
||||
}
|
||||
this.checkReady()
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 检查是否所有图片加载完毕
|
||||
*/
|
||||
checkReady() {
|
||||
if (this.root && !this.root.lazyLoad) {
|
||||
this.root._unloadimgs -= 1
|
||||
if (!this.root._unloadimgs) {
|
||||
setTimeout(() => {
|
||||
this.root.getRect().then(rect => {
|
||||
this.root.$emit('ready', rect)
|
||||
}).catch(() => {
|
||||
this.root.$emit('ready', {})
|
||||
})
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 链接点击事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
linkTap(e) {
|
||||
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
|
||||
const attrs = node.attrs || e
|
||||
const href = attrs.href
|
||||
this.root.$emit('linktap', Object.assign({
|
||||
innerText: this.root.getText(node.children || []) // 链接内的文本内容
|
||||
}, attrs))
|
||||
if (href) {
|
||||
if (href[0] === '#') {
|
||||
// 跳转锚点
|
||||
this.root.navigateTo(href.substring(1)).catch(() => {})
|
||||
} else if (href.split('?')[0].includes('://')) {
|
||||
// 复制外部链接
|
||||
if (this.root.copyLink) {
|
||||
// #ifdef H5
|
||||
window.open(href)
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
uni.setClipboardData({
|
||||
data: href,
|
||||
success: () =>
|
||||
uni.showToast({
|
||||
title: '链接已复制'
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openWeb(href)
|
||||
// #endif
|
||||
}
|
||||
} else {
|
||||
// 跳转页面
|
||||
uni.navigateTo({
|
||||
url: href,
|
||||
fail() {
|
||||
uni.switchTab({
|
||||
url: href,
|
||||
fail() {}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 错误事件
|
||||
* @param {Event} e
|
||||
*/
|
||||
mediaError(e) {
|
||||
const i = e.currentTarget.dataset.i
|
||||
const node = this.childs[i]
|
||||
// 加载其他源
|
||||
if (node.name === 'video' || node.name === 'audio') {
|
||||
let index = (this.ctrl[i] || 0) + 1
|
||||
if (index > node.src.length) {
|
||||
index = 0
|
||||
}
|
||||
if (index < node.src.length) {
|
||||
this.$set(this.ctrl, i, index)
|
||||
return
|
||||
}
|
||||
} else if (node.name === 'img') {
|
||||
// #ifdef H5 && VUE3
|
||||
if (this.opts[0] && !this.ctrl.load) return
|
||||
// #endif
|
||||
// 显示错误占位图
|
||||
if (this.opts[2]) {
|
||||
this.$set(this.ctrl, i, -1)
|
||||
}
|
||||
this.checkReady()
|
||||
}
|
||||
if (this.root) {
|
||||
this.root.$emit('error', {
|
||||
source: node.name,
|
||||
attrs: node.attrs,
|
||||
// #ifndef H5 && VUE3
|
||||
errMsg: e.detail.errMsg
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* a 标签默认效果 */
|
||||
._a {
|
||||
padding: 1.5px 0 1.5px 0;
|
||||
color: #366092;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* a 标签点击态效果 */
|
||||
._hover {
|
||||
text-decoration: underline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 图片默认效果 */
|
||||
._img {
|
||||
max-width: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* 内部样式 */
|
||||
|
||||
._block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
._b,
|
||||
._strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
._del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
._em,
|
||||
._i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
._h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
._h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
._h3 {
|
||||
font-size: 1.17em;
|
||||
}
|
||||
|
||||
._h5 {
|
||||
font-size: 0.83em;
|
||||
}
|
||||
|
||||
._h6 {
|
||||
font-size: 0.67em;
|
||||
}
|
||||
|
||||
._h1,
|
||||
._h2,
|
||||
._h3,
|
||||
._h4,
|
||||
._h5,
|
||||
._h6 {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
._image {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
._ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
._li {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
._ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
._ol,
|
||||
._ul {
|
||||
display: block;
|
||||
padding-left: 40px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
._q::before {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._q::after {
|
||||
content: '"';
|
||||
}
|
||||
|
||||
._sub {
|
||||
font-size: smaller;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
._sup {
|
||||
font-size: smaller;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
._thead,
|
||||
._tbody,
|
||||
._tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
._tr {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
._td,
|
||||
._th {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
._th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
._ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
._ul ._ul {
|
||||
margin: 0;
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
._ul ._ul ._ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
._abbr,
|
||||
._b,
|
||||
._code,
|
||||
._del,
|
||||
._em,
|
||||
._i,
|
||||
._ins,
|
||||
._label,
|
||||
._q,
|
||||
._span,
|
||||
._strong,
|
||||
._sub,
|
||||
._sup {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
._video {
|
||||
width: 300px;
|
||||
height: 225px;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<view id="_root" :class="(selectable?'_select ':'')+'_root'">
|
||||
<slot v-if="!nodes[0]" />
|
||||
<mp-node :imgList="imgList" v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable,previewImg]" name="span"></mp-node>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted, PropType, onBeforeUnmount, getCurrentInstance, watch, nextTick } from "vue"
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import Parser from "./pare";
|
||||
import mpNode from "./mp-node.vue"
|
||||
|
||||
/**
|
||||
* @displayName HTML
|
||||
* @exportName tm-html
|
||||
* @category 展示组件
|
||||
* @description html渲染组件,这是精简后的不是所有插件都带上容易让组件变大,如果想要全部的,可以去用作者的,它是引用第三方实现见:https://github.com/jin-yufeng/mp-html
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmHtml' });
|
||||
const { config } = useTmConfig()
|
||||
const emits = defineEmits(['load', 'ready', 'imgtap', 'linktap', 'play', 'error'])
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 窗口宽
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 窗口高
|
||||
*/
|
||||
height: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
selectable: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
lazyLoad: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 需要渲染的markdow或者html内容。
|
||||
*/
|
||||
loadingImg: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
errorImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showImgMenu: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
|
||||
previewImg: {
|
||||
type: [Boolean, Number],
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
|
||||
const _width = computed(() => covetUniNumber(props.width, config.unit))
|
||||
const _height = computed(() => covetUniNumber(props.height, config.unit))
|
||||
const nodes = ref([])
|
||||
const _videos = ref([])
|
||||
const imgList = ref([])
|
||||
const _in = ref<Record<string, any>>({})
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
|
||||
const inthis = (page, selector, scrollTop) => {
|
||||
if (page && selector && scrollTop) {
|
||||
_in.value = {
|
||||
page,
|
||||
selector,
|
||||
scrollTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const navigateTo = (id, offset) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
offset = offset || 0
|
||||
let deep = ' '
|
||||
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
deep = '>>>'
|
||||
// #endif
|
||||
|
||||
const selector = uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(_in.value ? _in.value.page : proxy)
|
||||
// #endif
|
||||
.select((_in.value ? _in.value.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
|
||||
if (_in.value) {
|
||||
selector.select(_in.value.selector).scrollOffset()
|
||||
.select(_in.value.selector).boundingClientRect()
|
||||
} else {
|
||||
// 获取 scroll-view 的位置和滚动距离
|
||||
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
|
||||
}
|
||||
selector.exec(res => {
|
||||
if (!res[0]) {
|
||||
reject(Error('Label not found'))
|
||||
return
|
||||
}
|
||||
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
|
||||
if (_in.value) {
|
||||
// scroll-view 跳转
|
||||
_in.value.page[_in.value.scrollTop] = scrollTop
|
||||
} else {
|
||||
// 页面跳转
|
||||
uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: 300
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getText = (nodesa) => {
|
||||
let text = '';
|
||||
(function traversal(nodesa) {
|
||||
for (let i = 0; i < nodesa.length; i++) {
|
||||
const node = nodesa[i]
|
||||
if (node.type === 'text') {
|
||||
text += node.text.replace(/&/g, '&')
|
||||
} else if (node.name === 'br') {
|
||||
text += '\n'
|
||||
} else {
|
||||
// 块级标签前后加换行
|
||||
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
|
||||
if (isBlock && text && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
}
|
||||
// 递归获取子节点的文本
|
||||
if (node.children) {
|
||||
traversal(node.children)
|
||||
}
|
||||
if (isBlock && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
} else if (node.name === 'td' || node.name === 'th') {
|
||||
text += '\t'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(nodesa || nodes.value)
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
const getRect = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(proxy)
|
||||
// #endif
|
||||
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
|
||||
})
|
||||
}
|
||||
|
||||
const pauseMedia = () => {
|
||||
for (let i = (_videos.value || []).length; i--;) {
|
||||
_videos.value[i].pause()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const setPlaybackRate = (rate) => {
|
||||
for (let i = (_videos.value || []).length; i--;) {
|
||||
_videos.value[i].playbackRate(rate)
|
||||
}
|
||||
}
|
||||
|
||||
const setContent = (content, append = false) => {
|
||||
if (!append || !imgList.value) {
|
||||
imgList.value = []
|
||||
}
|
||||
const par = new Parser({
|
||||
nodes:nodes.value,
|
||||
containerStyle:'',
|
||||
domain:'',
|
||||
useAnchor:0,
|
||||
lazyLoad:props.lazyLoad,
|
||||
pauseVideo:false,
|
||||
setTitle:false,
|
||||
scrollTable:false,
|
||||
selectable:props.selectable,
|
||||
previewImg:props.previewImg
|
||||
})
|
||||
const nodesas = par.parse(content)
|
||||
imgList.value = par.imgList
|
||||
|
||||
|
||||
nodes.value = append ? (nodes.value || []).concat(nodesas) : nodesas
|
||||
_videos.value = []
|
||||
nextTick(() => {
|
||||
emits('load')
|
||||
})
|
||||
|
||||
if (props.lazyLoad || imgList.value._unloadimgs < imgList.value.length / 2) {
|
||||
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
|
||||
let height = 0
|
||||
const callback = rect => {
|
||||
if (!rect || !rect.height) rect = {}
|
||||
// 350ms 总高度无变化就触发 ready 事件
|
||||
if (rect.height === height) {
|
||||
emits('ready', rect)
|
||||
} else {
|
||||
height = rect.height
|
||||
setTimeout(() => {
|
||||
getRect().then(callback).catch(callback)
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
getRect().then(callback).catch(callback)
|
||||
} else {
|
||||
// 未设置懒加载,等待所有图片加载完毕
|
||||
if (!imgList.value._unloadimgs) {
|
||||
getRect().then(rect => {
|
||||
emits('ready', rect)
|
||||
}).catch(() => {
|
||||
emits('ready', {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
watch(() => props.value, () => {
|
||||
setContent(props.value)
|
||||
})
|
||||
onMounted(() => {
|
||||
if (props.value && !nodes.value.length) {
|
||||
setContent(props.value)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
._root {
|
||||
padding: 1px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 长按复制 */
|
||||
._select {
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<text
|
||||
@tap="onclick"
|
||||
v-if="!isImg"
|
||||
:class="['tmIcon', `${prefix}${iconName}`, isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{
|
||||
fontSize: iconSize,
|
||||
color: fontColor,
|
||||
fontFamily: fontFamily,
|
||||
fontStyle: 'normal',
|
||||
},
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></text>
|
||||
<image
|
||||
v-if="isImg"
|
||||
@tap="onclick"
|
||||
:src="iconName"
|
||||
:class="['tmIcon', isSpin?'tmIconSpin':'',_class]"
|
||||
:style="[
|
||||
{ width: iconSize, height: iconSize },
|
||||
rotateDeg ? { transform: `rotate(${rotateDeg})` } : {},
|
||||
_style
|
||||
]"
|
||||
></image>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { covetUniNumber } from "../../libs/tool";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { getDefaultColor } from "../../libs/colors";
|
||||
|
||||
/**
|
||||
* @displayName 图标
|
||||
* @exportName tm-icon
|
||||
* @category 常用组件
|
||||
* @description 图标,可以是图片地址或者字体名称图标.也可以是你自定义的图标.
|
||||
* 如何自定义图标呢:你可以在iconfont网站勾选图标->添加购物车->添加项目->项目设置,勾选woff2,勾选base64->下载本地->解压得到iconfont.css
|
||||
* ->复制到你的app.vue全局中即可.然后使用图标时,font-family是你的图标名称比如myicon,prefix是图标前缀比如myicon-
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: "TmIcon" });
|
||||
const { config } = useTmConfig();
|
||||
const emits = defineEmits([
|
||||
/**
|
||||
* 点击事件
|
||||
*/
|
||||
"click",
|
||||
]);
|
||||
const attrs = defineProps({
|
||||
_style:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
_class:{
|
||||
type:String,
|
||||
default:""
|
||||
},
|
||||
/**
|
||||
* 图标名称,可以是图片地址
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* 图标大小,默认32rpx
|
||||
*/
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: "32",
|
||||
},
|
||||
/**
|
||||
* 图标颜色
|
||||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "#333333",
|
||||
},
|
||||
/**
|
||||
* 暗黑时的图标颜色
|
||||
*/
|
||||
darkColor: {
|
||||
type: String,
|
||||
default: "white",
|
||||
},
|
||||
/**
|
||||
* 图标是否旋转
|
||||
*/
|
||||
spin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
rotate: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
/**字体图标名称,如果你要显示你自己的名称时,需要改成你自己的图标名称. */
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: "remixicon",
|
||||
},
|
||||
/**图标名称前缀,默认为ri-,没有你可以为空 */
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "ri-",
|
||||
},
|
||||
});
|
||||
|
||||
const iconName = computed(() => attrs.name);
|
||||
const iconSize = computed(() => covetUniNumber(attrs.size, "rpx"));
|
||||
const isImg = computed(() => iconName.value.includes("."));
|
||||
const isSpin = computed(() => attrs.spin);
|
||||
const rotateDeg = computed(() => attrs.rotate);
|
||||
const _style = computed(() => attrs._style);
|
||||
const _class = computed(() => attrs._class);
|
||||
const fontColor = computed(() => {
|
||||
let color = attrs.color;
|
||||
if (config.mode == "dark" && attrs.darkColor) {
|
||||
color = attrs.darkColor;
|
||||
}
|
||||
return getDefaultColor(color);
|
||||
});
|
||||
const onclick = () => emits("click");
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tmIcon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
transition: transform 0.3s linear;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.tmIconSpin {
|
||||
animation: tmIconAniSPin 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes tmIconAniSPin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,399 @@
|
||||
<template>
|
||||
<view @click="prevImage" class="tmImage" :class="[idBox]" ref="tmImage" :id="idBox"
|
||||
:style="{ width: _place_size.width, height: _place_size.height }">
|
||||
<!-- @vue-skip -->
|
||||
<view class="tmImageBox"
|
||||
:style="{ width: _img_box_size.width, height: _img_box_size.height, borderRadius: _round, pointerEvent: 'none' }">
|
||||
<view v-if="isLoading || isError" class="tmImagePlace" :style="{ backgroundColor: _placeBgColor }">
|
||||
<tm-icon :size="iconSize" v-if="isError" color="error" name="landscape-line"></tm-icon>
|
||||
<tm-icon :size="iconSize" v-if="isLoading" name="loader-line" color="primary" :spin="true"></tm-icon>
|
||||
</view>
|
||||
|
||||
<image v-if="!isError" class="tmImageImg" :lazy-load="lazy" :draggable="draggable"
|
||||
:showMenuByLongpress="showMenuByLongpress"
|
||||
:class="[isLoading ? 'tmImageImgAbs' : '', fadeShow ? 'tmImageFadeShow' : '']" :mode="_model"
|
||||
:style="[_styleMap]" :src="_src" @error="imgError">
|
||||
</image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, onUpdated, nextTick } from 'vue';
|
||||
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
|
||||
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj } from "../../libs/colors";
|
||||
import { useTmConfig } from "../../libs/config";
|
||||
import { onPageScroll } from '@dcloudio/uni-app';
|
||||
/**
|
||||
* @displayName 图片
|
||||
* @exportName tm-image
|
||||
* @category 展示组件
|
||||
* @description 宽高可以设置,支持百分比,px,rpx,如果你用了auto,%这种就要注意:如果你把组件放到了容器中,容器设置为v-show,display:none,visible这种,
|
||||
* 那你一定要跟随vif组件,不然尺寸是不对的。因为小程序无法监测这种变化的情况,导致无法设置尺寸。如果你设定了固定尺寸则不用担心这个问题。
|
||||
* @constant 平台兼容
|
||||
* | H5 | uniAPP | 小程序 | version |
|
||||
| --- | --- | --- | --- |
|
||||
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
|
||||
*/
|
||||
defineOptions({ name: 'TmBadge' });
|
||||
const { config } = useTmConfig()
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 宽度,默认100%
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: "100%"
|
||||
},
|
||||
/**
|
||||
* 高度,auto,%,rpx,px,string number
|
||||
* 18rpx,18px,15%支持这三种单位,如果只写"18"就表示18rpx
|
||||
*/
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: "auto"
|
||||
},
|
||||
/** 图片源 */
|
||||
src: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 模式
|
||||
* @link https://uniapp.dcloud.net.cn/uni-app-x/component/image.html#mode-values
|
||||
*/
|
||||
model: {
|
||||
type: String as PropType<"fill" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "scaleToFill">,
|
||||
default: "fill"
|
||||
},
|
||||
/**
|
||||
* 点击后是否预览图片
|
||||
*/
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 预览占位比例
|
||||
* 宽/高,当数据没加载前,如果你设置了一项值比如宽,高会自动根据这个比例计算
|
||||
* 当图片加载成功后,使用正确的原图片比例设置。
|
||||
* 默认是5/4=1.25
|
||||
*/
|
||||
ratio: {
|
||||
type: Number,
|
||||
default: 1.25
|
||||
},
|
||||
/**
|
||||
* 圆角
|
||||
*/
|
||||
round: {
|
||||
type: [String, Number],
|
||||
default: '0'
|
||||
},
|
||||
/**
|
||||
* 加载和失败时的图标大小。
|
||||
*/
|
||||
iconSize: {
|
||||
type: [String, Number],
|
||||
default: "36"
|
||||
},
|
||||
/**
|
||||
* 占位背景色
|
||||
*/
|
||||
placeBgColor: {
|
||||
type: String,
|
||||
default: "#F5F5F5"
|
||||
},
|
||||
/**
|
||||
* 点位暗黑时的背景,如果不填写默认填充inputDarkBgcolor
|
||||
*/
|
||||
placeDarkBgColor: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
/**
|
||||
* 是否显示过渡动画
|
||||
*/
|
||||
fadeShow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 图片懒加载
|
||||
*/
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 是否允许拖动。
|
||||
*/
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showMenuByLongpress: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>();
|
||||
|
||||
const id = ref(`tmImage-${getUid()}`);
|
||||
const idBox = ref(`tmImage-${getUid()}`);
|
||||
const isLoading = ref(true);
|
||||
const isError = ref(false);
|
||||
const reload = ref(0);
|
||||
const imgrealWidth = ref(0);
|
||||
const imgrealHeight = ref(0);
|
||||
const boxWidth = ref(0);
|
||||
const boxHeight = ref(0);
|
||||
|
||||
const isLoaded = ref(false);
|
||||
const tid = ref(0);
|
||||
const androidAndWebUrl = ref("");
|
||||
|
||||
const tid2 = ref(0);
|
||||
const proxy = getCurrentInstance()?.proxy
|
||||
|
||||
const _model = computed(() => props.model);
|
||||
const _placeBgColor = computed(() => {
|
||||
let bgcolor = props.placeBgColor;
|
||||
if (config.mode == 'dark') {
|
||||
bgcolor = props.placeDarkBgColor;
|
||||
if (props.placeDarkBgColor == '') {
|
||||
bgcolor = config.inputDarkColor;
|
||||
}
|
||||
}
|
||||
return getDefaultColor(bgcolor);
|
||||
});
|
||||
|
||||
const _round = computed(() => covetUniNumber(props.round, config.unit));
|
||||
const _src = computed(() => props.src);
|
||||
|
||||
const _place_size = computed(() => ({
|
||||
width: covetUniNumber(props.width, config.unit),
|
||||
height: covetUniNumber(props.height, config.unit),
|
||||
}));
|
||||
|
||||
|
||||
|
||||
const _img_box_size = computed(() => {
|
||||
if (imgrealHeight.value > 0) {
|
||||
return _img_size.value;
|
||||
}
|
||||
|
||||
let _w = props.width;
|
||||
let _h = props.height;
|
||||
let us_w = covetUniNumber(props.width, config.unit);
|
||||
let us_h = covetUniNumber(props.height, config.unit);
|
||||
|
||||
if (typeof props.width == 'string' && props.width.lastIndexOf('%') > -1 || props.width == 'auto') {
|
||||
us_w = '100%';
|
||||
_w = '100%';
|
||||
}
|
||||
|
||||
if (typeof props.height == 'string' && props.height.lastIndexOf('%') > -1 || props.height == 'auto' || isError.value) {
|
||||
if (boxHeight.value >= 5) {
|
||||
_h = `${boxHeight.value}px`;
|
||||
} else {
|
||||
if (typeof props.width == 'string' &&props.width.lastIndexOf('%') > -1 || props.width == 'auto') {
|
||||
if (props.height == '100%') {
|
||||
_h = `${props.ratio * boxHeight.value}px`;
|
||||
} else {
|
||||
_h = `${props.ratio * boxWidth.value}px`;
|
||||
}
|
||||
} else {
|
||||
_h = `${props.ratio * parseFloat(us_w)}${getUnit(us_w)}`;
|
||||
}
|
||||
}
|
||||
return { width: _w, height: _h };
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h };
|
||||
});
|
||||
|
||||
const _img_size = computed(() => {
|
||||
let us_w = covetUniNumber(props.width, config.unit);
|
||||
let us_h = covetUniNumber(props.height, config.unit);
|
||||
|
||||
if (!isLoaded.value) {
|
||||
return { width: "300px", height: "300px" };
|
||||
}
|
||||
|
||||
if (boxWidth.value > 0) {
|
||||
let ratio = boxWidth.value / imgrealWidth.value;
|
||||
|
||||
if (props.height == 'auto') {
|
||||
us_h = `${ratio * imgrealHeight.value}px`;
|
||||
}
|
||||
|
||||
if (props.width == 'auto') {
|
||||
us_w = `${boxWidth.value}px`;
|
||||
}
|
||||
|
||||
if (typeof props.height == 'string'&&props.height.lastIndexOf('%') > -1) {
|
||||
us_h = `${boxHeight.value}px`;
|
||||
}
|
||||
if (typeof props.width == 'string'&&props.width.lastIndexOf('%') > -1) {
|
||||
us_w = `${boxWidth.value}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return { width: us_w, height: us_h };
|
||||
});
|
||||
|
||||
const _styleMap = computed(() => {
|
||||
let styleMap: Record<string, any> = {}
|
||||
styleMap["width"] = _img_size.value.width
|
||||
styleMap["height"] = _img_size.value.height
|
||||
styleMap["transform"] = isLoading.value ? 'scale(0.1)' : 'scale(1)'
|
||||
styleMap["visibility"] = isLoading.value ? 'visible' : (!isError.value ? 'visible' : 'hidden')
|
||||
styleMap["opacity"] = isLoading.value ? '0' : '1'
|
||||
styleMap["border-radius"] = _round.value
|
||||
return styleMap;
|
||||
});
|
||||
|
||||
const prevImage = () => {
|
||||
emit('click');
|
||||
if (props.preview) {
|
||||
uni.previewImage({
|
||||
current: props.src,
|
||||
urls: [props.src]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const imgLoad = () => {
|
||||
uni.getImageInfo({
|
||||
src: _src.value,
|
||||
fail(_) {
|
||||
console.log('error');
|
||||
isError.value = true;
|
||||
isLoading.value = false;
|
||||
},
|
||||
success(result: any) {
|
||||
isLoading.value = false;
|
||||
isError.value = false;
|
||||
isLoaded.value = true;
|
||||
imgrealWidth.value = result.width;
|
||||
imgrealHeight.value = result.height;
|
||||
androidAndWebUrl.value = result.path;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const imgError = () => {
|
||||
isError.value = true;
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const getNodes = (): Promise<any> => {
|
||||
return new Promise((res) => {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".tmImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as UniApp.NodeInfo;
|
||||
boxWidth.value = nodeinfo.width!;
|
||||
boxHeight.value = nodeinfo.height!;
|
||||
res(true)
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
const resize = (): Promise<any> => {
|
||||
isLoaded.value = false;
|
||||
return new Promise((res) => {
|
||||
uni.createSelectorQuery().in(proxy)
|
||||
.select(".tmImage")
|
||||
.boundingClientRect().exec((ret) => {
|
||||
let nodeinfo = ret[0] as UniApp.NodeInfo;
|
||||
boxWidth.value = nodeinfo.width!;
|
||||
boxHeight.value = nodeinfo.height!;
|
||||
isLoaded.value = true;
|
||||
res(true)
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getNodes();
|
||||
imgLoad();
|
||||
|
||||
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(tid.value);
|
||||
clearTimeout(tid2.value);
|
||||
});
|
||||
|
||||
watch(() => props.src, () => {
|
||||
imgLoad();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "apply-shared",
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
multipleSlots: true,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.tmImage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.tmImageBox {
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.tmImageImg {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes animateFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tmImageFadeShow {
|
||||
animation: animateFadeIn 0.2s linear forwards;
|
||||
}
|
||||
|
||||
.tmImagePlace {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.tmImageImgAbs {
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
</style>
|
||||