feat:微信小程序一键登录页面

This commit is contained in:
dzc committed 2026-03-09 17:25:01 +08:00
1 parent 17ca43dc6c
commit 955c13429a
152 files changed
+37880 -3300

No files matched your search

+56 -12
View File
@@ -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>
+10
View File
@@ -0,0 +1,10 @@
declare const __TABBAR__: Array<string>
declare const __ROUTES__: Array<any>
declare namespace GlobalSetting {
// 记住密码
type RememberUser = {
username: string
password: string
}
}
+17 -6
View File
@@ -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,
}
}
+61 -6
View File
@@ -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"
}
]
}
}
}
-41
View File
@@ -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>
+116
View File
@@ -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>
+100
View File
@@ -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>
+407
View File
@@ -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>
+99
View File
@@ -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>
+25
View File
@@ -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,
},
}),
}
+64
View File
@@ -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,
}),
}
+10
View File
@@ -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,
}),
}
+40
View File
@@ -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,
}),
}
+34
View File
@@ -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,
}),
}
+33
View File
@@ -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,
})
},
}
+15
View File
@@ -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"
+39
View File
@@ -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",
}),
}
+79
View File
@@ -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,
}),
}
+68
View File
@@ -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,
})
},
}
+11
View File
@@ -0,0 +1,11 @@
import request from "@/sheep/utils/request"
export default {
get: (params) => {
return request({
url: "/canteen/canteenOnlineSpu/app/getCanteenSpuDetails",
method: "GET",
params,
})
},
}
+32
View File
@@ -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,
})
},
}
+31
View File
@@ -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,
}),
}
+22
View File
@@ -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,
}),
}
+139
View File
@@ -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,
}),
}
+94
View File
@@ -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>
+76
View File
@@ -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>
+6
View File
@@ -0,0 +1,6 @@
export default {
// 微信小程序
WECHAT: "wechat",
// 支付宝
ALI_PAY: "ali_pay",
} as const
+5
View File
@@ -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"
+67
View File
@@ -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,
}
+9
View File
@@ -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",
}
+9
View File
@@ -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"
+5
View File
@@ -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" });
};
+11
View File
@@ -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];
};
+40
View File
@@ -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,
}
}
+7
View File
@@ -0,0 +1,7 @@
export const usePreviewImage = (str: string | undefined) => {
if (str == undefined) return
let urlList = str.split(",")
uni.previewImage({
urls: urlList,
})
}
+72
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
export const useMakePhoneCall = (str: string | undefined) => {
if (str == undefined) return;
uni.makePhoneCall({ phoneNumber: str });
};
+124
View File
@@ -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 }
}
+50
View File
@@ -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
}
+66
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./plugs/index"
export * as router from "./router"
export * as constant from "./constant"
export * as hooks from "./hooks"
+1
View File
@@ -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),
},
}
},
}
}
+83
View File
@@ -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
}
}
+103
View File
@@ -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";
}
+17
View File
@@ -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';
}
+35
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
import { createPinia } from "pinia"
const pinia = createPinia()
export default pinia
+78
View File
@@ -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" })
},
},
})
+33
View File
@@ -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
},
},
})
+74
View File
@@ -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" })
},
},
})
+23
View File
@@ -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"
}
+24
View File
@@ -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)}`
}
+21
View File
@@ -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)
}
+145
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

+13
View File
@@ -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>
&nbsp;&nbsp;
<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>
File diff suppressed because it is too large. Load diff
@@ -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(/&amp;/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>
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more