This commit is contained in:
dzc committed 2024-11-11 14:42:47 +08:00
commit 7d450caf77
146 files changed
+39809

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
<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>
<style lang="scss">
@import "@/sheep/scss/global.scss";
@import url(@/uni_modules/tm-ui/css/remixicon.min.css);
</style>
+42
View File
@@ -0,0 +1,42 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
PickerItem: typeof import('./src/uni_modules/tm-ui/components/tm-picker-view/picker-item.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TmAlert: typeof import('./src/uni_modules/tm-ui/components/tm-alert/tm-alert.vue')['default']
TmBadge: typeof import('./src/uni_modules/tm-ui/components/tm-badge/tm-badge.vue')['default']
TmBetweenTime: typeof import('./src/uni_modules/tm-ui/components/tm-between-time/tm-between-time.vue')['default']
TmButton: typeof import('./src/uni_modules/tm-ui/components/tm-button/tm-button.vue')['default']
TmCell: typeof import('./src/uni_modules/tm-ui/components/tm-cell/tm-cell.vue')['default']
TmCheckbox: typeof import('./src/uni_modules/tm-ui/components/tm-checkbox/tm-checkbox.vue')['default']
TmCheckboxGroup: typeof import('./src/uni_modules/tm-ui/components/tm-checkbox-group/tm-checkbox-group.vue')['default']
TmCol: typeof import('./src/uni_modules/tm-ui/components/tm-col/tm-col.vue')['default']
TmCollapse: typeof import('./src/uni_modules/tm-ui/components/tm-collapse/tm-collapse.vue')['default']
TmCollapseItem: typeof import('./src/uni_modules/tm-ui/components/tm-collapse-item/tm-collapse-item.vue')['default']
TmDateView: typeof import('./src/uni_modules/tm-ui/components/tm-date-view/tm-date-view.vue')['default']
TmDrawer: typeof import('./src/uni_modules/tm-ui/components/tm-drawer/tm-drawer.vue')['default']
TmIcon: typeof import('./src/uni_modules/tm-ui/components/tm-icon/tm-icon.vue')['default']
TmImage: typeof import('./src/uni_modules/tm-ui/components/tm-image/tm-image.vue')['default']
TmInput: typeof import('./src/uni_modules/tm-ui/components/tm-input/tm-input.vue')['default']
TmModal: typeof import('./src/uni_modules/tm-ui/components/tm-modal/tm-modal.vue')['default']
TmPicker: typeof import('./src/uni_modules/tm-ui/components/tm-picker/tm-picker.vue')['default']
TmPickerDate: typeof import('./src/uni_modules/tm-ui/components/tm-picker-date/tm-picker-date.vue')['default']
TmPickerView: typeof import('./src/uni_modules/tm-ui/components/tm-picker-view/tm-picker-view.vue')['default']
TmRadio: typeof import('./src/uni_modules/tm-ui/components/tm-radio/tm-radio.vue')['default']
TmRadioGroup: typeof import('./src/uni_modules/tm-ui/components/tm-radio-group/tm-radio-group.vue')['default']
TmRow: typeof import('./src/uni_modules/tm-ui/components/tm-row/tm-row.vue')['default']
TmSheet: typeof import('./src/uni_modules/tm-ui/components/tm-sheet/tm-sheet.vue')['default']
TmSwitch: typeof import('./src/uni_modules/tm-ui/components/tm-switch/tm-switch.vue')['default']
TmTabs: typeof import('./src/uni_modules/tm-ui/components/tm-tabs/tm-tabs.vue')['default']
TmTabsItem: typeof import('./src/uni_modules/tm-ui/components/tm-tabs-item/tm-tabs-item.vue')['default']
TmTag: typeof import('./src/uni_modules/tm-ui/components/tm-tag/tm-tag.vue')['default']
TmText: typeof import('./src/uni_modules/tm-ui/components/tm-text/tm-text.vue')['default']
}
}
+8
View File
@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
+3
View File
@@ -0,0 +1,3 @@
declare const __TABBAR__: Array<string>;
declare const __ROUTES__: Array<any>;
declare const __APP_SERVICE__: string;
+18
View File
@@ -0,0 +1,18 @@
import { createSSRApp } from "vue";
import "uno.css";
import * as Pinia from "pinia";
import App from "./App.vue";
import tmUi from "./uni_modules/tm-ui";
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
app.use(tmUi);
return {
app,
Pinia,
};
}
+72
View File
@@ -0,0 +1,72 @@
{
"name" : "",
"appid" : "",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* 模块配置 */
"modules" : {},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios打包配置 */
"ios" : {},
/* SDK配置 */
"sdkConfigs" : {}
}
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics": {
"enable": false
},
"vueVersion" : "3"
}
+152
View File
@@ -0,0 +1,152 @@
{
"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/tabbar/home",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "pages/tabbar/food",
"style": {
"navigationBarTitleText": "菜谱",
"disableScroll": true
}
},
{
"path": "pages/tabbar/order",
"style": {
"navigationBarTitleText": "订单",
"disableScroll": true
}
},
{
"path": "pages/tabbar/my",
"style": {
"navigationBarTitleText": "我的",
"disableScroll": true
}
}
],
"subPackages": [
{
"root": "pages/order",
"pages": []
},
{
"root": "pages/shop",
"pages": []
},
{
"root": "pages/apply",
"pages": [
{
"path": "foreign-apply",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "foreign-list",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "foreign-items",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "stride-apply",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "stride-list",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
}
]
},
{
"root": "pages/wallet",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "recharge",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
}
]
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "浦江小程序",
"navigationBarBackgroundColor": "#1777FF",
"backgroundColor": "#ecf4f9",
"navigationStyle": "custom"
},
"tabBar": {
"list": [
{
"pagePath": "pages/tabbar/home",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-select.png"
},
{
"pagePath": "pages/tabbar/food",
"text": "菜谱",
"iconPath": "static/tabbar/food.png",
"selectedIconPath": "static/tabbar/food-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"
}
]
}
}
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import dayjs from "dayjs"
import { hooks, router } from "@@/index"
import { applyApi } from "@@/api"
const state = reactive<any>({
})
const applyForm = reactive<any>({
displayTimeRange: "",
displayTimeRangeStr: "",
count: 1,
remark: "",
})
const applyFormRules = reactive<any>({
displayTimeRangeStr: [
{ required: true, message: '请选择使用时间' }
],
count: [
{ required: true, message: '请输入申请数量' },
{ required: true, min: 1, message: '最少申请1张' },
{ required: true, max: 20, message: '最多申请20张' },
]
})
onLoad(() => {
});
onShow(() => {
})
const applyFormRef = ref<any>(null)
const doSubmit = async () => {
applyFormRef.value.submit()
const { isPass } = applyFormRef.value.validate()
if (!isPass) return
const range = applyForm.displayTimeRange
try {
uni.showLoading({ title: '申请中...', mask: false });
const _pa = {
applyStartTime: dayjs(range[0]).valueOf(),
applyEndTime: dayjs(range[1]).valueOf(),
count: applyForm.count,
remark: applyForm.remark,
}
const { data, code }: any = await applyApi.foreignApply(_pa)
if (code != 0) throw new Error()
uni.hideLoading()
uni.showToast({ title: '申请成功', icon: "none" });
router.back()
} catch (error) {
console.error(error);
uni.hideLoading()
}
}
</script>
<template>
<crud-layout title="外来人员就餐" bg="/static/page/blue-bg.png">
<view class="h-30rpx" />
<tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical">
<tm-form-item name="displayTimeRangeStr" label="使用时间" :showBottom="false">
<tm-between-time start="2020-1-1" end="2030-1-1" v-model="applyForm.displayTimeRange"
v-model:model-str="applyForm.displayTimeRangeStr" format="YYYY-MM-DD">
<view style="pointer-events: none;">
<tm-input v-model="applyForm.displayTimeRangeStr" disabled placeholder="请选择使用时间..">
<template v-slot:rightIcon>
<text class="p-0.5rem color-#b1b1b1 iconfont icon-xiangxiajiantouxiao"></text>
</template>
</tm-input>
</view>
</tm-between-time>
</tm-form-item>
<view class="h-30rpx" />
<tm-form-item name="count" label="申请数量" :showBottom="false">
<tm-input v-model="applyForm.count" type="number" showClear placeholder="请输入申请数量.." />
</tm-form-item>
<view class="h-30rpx" />
<tm-form-item name="remark" label="备注" :required="false" :showBottom="false">
<tm-input v-model="applyForm.remark" type="textarea" showClear placeholder="请输入备注.."></tm-input>
</tm-form-item>
</tm-form>
<view class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-60rpx sub-text-30 color-white!"
@tap="doSubmit">
提交
</view>
</crud-layout>
</template>
<style lang="scss" scoped>
:deep() {
.tmFormItemLabel {
padding-bottom: 10rpx !important;
}
}
</style>
+72
View File
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { isEmpty } from 'radash'
import { hooks, router } from "@@/index"
import { applyApi } from "@@/api"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
const data = {
tabs: ['待审核', '已通过', '已拒绝'],
}
const state = reactive<any>({
pa: {
id: null,
pagesize: 99,
},
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, applyApi.foreignItems, null, 'data')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
onLoad((options: any) => {
if (!isEmpty(options.id)) {
state.pa.id = options.id
}
});
onShow(() => {
})
const doTab = (e: any) => {
if (state.pa.status != e + 1) {
state.pa.status = e + 1
pageRef.value.reload(true)
}
}
</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="5"
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">
<template #top>
<view class="h-30rpx" />
</template>
<view class="flex-col card p-x-20rpx p-y-30rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i">
<view class="flex-row items-center justify-between">
<view class="">
<tm-tag v-if="v.status == 1" size="m" color="info">未使用</tm-tag>
<tm-tag v-if="v.status == 2" size="m" color="success">已使用</tm-tag>
<view class="m-t-10rpx sub-text-26">
<text>{{ v.userNameStr }}</text>
<text class="m-l-10rpx">{{ v.userMobile }}</text>
</view>
</view>
<image class="w-100rpx h-100rpx" :src="v.code" mode="aspectFill" />
</view>
</view>
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+97
View File
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { hooks, router } from "@@/index"
import { applyApi } from "@@/api"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
const data = {
tabs: ['待审核', '已通过', '已拒绝'],
}
const state = reactive<any>({
pa: {
status: 1,
},
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, applyApi.foreignList, null, 'data.list')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
onLoad(() => {
});
onShow(() => {
pageRef.value.reload(false)
})
const doTab = (e: any) => {
if (state.pa.status != e + 1) {
state.pa.status = e + 1
pageRef.value.reload(true)
}
}
const doApply = async () => {
router.jump("/pages/apply/foreign-apply")
}
const doCard = (v: any) => {
if (v.status == 2) {
router.jump("/pages/apply/foreign-items", { id: v.id })
}
}
</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="5"
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">
<template #top>
<view class="h-30rpx" />
<view class="bg-white rounded-16rpx px-20rpx">
<z-tabs :list="data.tabs" @change="doTab" />
</view>
</template>
<view class="h-20rpx" />
<view class="flex-col card p-x-20rpx p-y-30rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i"
@tap="doCard(v)">
<view class="flex-row items-center justify-between">
<view class="sub-text-28">电子餐券</view>
<view class="sub-text-24">{{ hooks.useTime(v.createTime, "") }}</view>
</view>
<view class="sub-text-24">
<view class="m-t-20rpx">
<text>有效期</text>
<text class="m-l-10rpx">
{{ hooks.useTime(v.applyStartTime, " {y}-{m}-{d}") }}
~
{{ hooks.useTime(v.applyEndTime, "{y}-{m}-{d}") }}
</text>
</view>
<view class="m-t-20rpx">
<text>数量</text>
<text class="m-l-10rpx">{{ v.count }}</text>
</view>
</view>
</view>
<template #bottom>
<view
class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-60rpx sub-text-30 color-white!"
@tap="doApply">
申请
</view>
<view class="h-30rpx" />
</template>
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+69
View File
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { hooks, router } from "@@/index"
import { applyApi } from "@@/api"
const state = reactive<any>({
})
const applyForm = reactive<any>({
remark: "",
})
const applyFormRules = reactive<any>({
})
onLoad(() => {
});
onShow(() => {
})
const applyFormRef = ref<any>(null)
const doSubmit = async () => {
applyFormRef.value.submit()
const { isPass } = applyFormRef.value.validate()
if (!isPass) return
const range = applyForm.displayTimeRange
try {
uni.showLoading({ title: '申请中...', mask: false });
const _pa = {
remark: applyForm.remark,
}
const { data, code }: any = await applyApi.strideApply(_pa)
if (code != 0) throw new Error()
uni.hideLoading()
uni.showToast({ title: '申请成功', icon: "none" });
router.back()
} catch (error) {
console.error(error);
uni.hideLoading()
}
}
</script>
<template>
<crud-layout title="外来人员就餐" bg="/static/page/blue-bg.png">
<view class="h-30rpx" />
<tm-form ref="applyFormRef" v-model="applyForm" :rules="applyFormRules" direction="vertical">
<tm-form-item name="remark" label="备注" :required="false" :showBottom="false">
<tm-input v-model="applyForm.remark" type="textarea" showClear placeholder="请输入备注.."></tm-input>
</tm-form-item>
</tm-form>
<view class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-60rpx sub-text-30 color-white!"
@tap="doSubmit">
提交
</view>
</crud-layout>
</template>
<style lang="scss" scoped>
:deep() {
.tmFormItemLabel {
padding-bottom: 10rpx !important;
}
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { hooks, router } from "@@/index"
import { applyApi } from "@@/api"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
const data = {
tabs: ['待审核', '已通过', '已拒绝'],
}
const state = reactive<any>({
pa: {
status: 1,
},
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, applyApi.strideList, null, 'data.list')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
onLoad(() => {
});
onShow(() => {
})
const doTab = (e: any) => {
if (state.pa.status != e + 1) {
state.pa.status = e + 1
pageRef.value.reload(true)
}
}
const doApply = async () => {
router.jump("/pages/apply/stride-apply")
}
</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="5"
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">
<template #top>
<view class="h-30rpx" />
<view class="bg-white rounded-16rpx px-20rpx">
<z-tabs :list="data.tabs" @change="doTab" />
</view>
</template>
<view class="flex-col card p-x-20rpx p-y-30rpx m-b-20rpx" v-for="(v, i) in pageHook.list" :key="i">
</view>
<template #bottom>
<view
class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-60rpx sub-text-30 color-white!"
@tap="doApply">
申请
</view>
<view class="h-30rpx" />
</template>
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { hooks } from "@@/index"
import { merchantApi } from "@@/api"
const state = reactive<any>({
})
onLoad(() => {
});
onShow(() => {
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, merchantApi.merchantList, null, 'data')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
</script>
<template>
<crud-layout>
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5"
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">
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { hooks } from "@@/index"
import { merchantApi } from "@@/api"
const state = reactive<any>({
})
onLoad(() => {
});
onShow(() => {
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, merchantApi.merchantList, null, 'data')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
</script>
<template>
<crud-layout>
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5"
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">
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+122
View File
@@ -0,0 +1,122 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { isEmpty } from "radash";
import { hooks, router, constant } from "@@/index"
import { merchantApi, userApi, walletApi } from "@@/api"
const data = {
serviceList: [
{ text: '钱包管理', icon: '/static/icon/service_3.png', router: '/pages/wallet/list' },
{ text: '车牌管理', icon: '/static/icon/service_2.png', router: '/pages/user/diff-price' },
{ text: '外来人员就餐', icon: '/static/icon/service_1.png', router: '/pages/apply/foreign-list' },
{ text: '跨单位就餐', icon: '/static/icon/service_1.png', router: '/pages/apply/stride-list' },
]
};
const state = reactive<any>({
wallet: [],
userInfo: {}
})
onLoad(() => {
});
onShow(() => {
getUserInfo()
})
const getUserInfo = () => {
const userInfoKey = constant.storageConstant.USERINFO
const userStorage = uni.getStorageSync(userInfoKey)
if (!isEmpty(userStorage)) {
state.userInfo = userStorage
}
}
const doLogin = async () => {
const tokenKey = constant.storageConstant.TOKEN
const userInfoKey = constant.storageConstant.USERINFO
const tokenStorage = uni.getStorageSync(tokenKey)
const userStorage = uni.getStorageSync(userInfoKey)
if (isEmpty(tokenStorage) || isEmpty(userStorage)) {
uni.showLoading({
title: '登录中...',
mask: true
});
const { data: authData } = await userApi.auth("test");
uni.setStorageSync(tokenKey, authData)
const { data: userData } = await userApi.userInfo();
uni.setStorageSync(userInfoKey, userData)
getUserInfo()
uni.hideLoading()
uni.showToast({ title: '登录成功', icon: "none" });
}
}
const doLogout = async () => {
uni.showLoading({
title: '退出登录中...',
mask: true
});
const tokenKey = constant.storageConstant.TOKEN
const userInfoKey = constant.storageConstant.USERINFO
uni.removeStorageSync(tokenKey)
uni.removeStorageSync(userInfoKey)
state.userInfo = {}
uni.hideLoading()
uni.showToast({ title: '退出登录成功', icon: "none" });
}
</script>
<template>
<crud-layout title="我的" bg="/static/page/blue-bg.png">
<view class="flex-row items-center">
<image class="w-100rpx h-100rpx rounded-full" v-if="state.userInfo.avatar" :src="state.userInfo.avatar"
mode="aspectFill" />
<image class="w-100rpx h-100rpx rounded-full" v-else src="/static/icon/default-avatar.png"
mode="aspectFill" />
<view class="m-l-30rpx" v-if="state.userInfo.id">
<view class="flex-row items-center">
<text class="sub-text-30">{{ state.userInfo.nickname }}</text>
<text class="iconfont icon-youjiantou color-#666666 m-l-20rpx"></text>
</view>
<text class="sub-text-26">ID {{ state.userInfo.id }}</text>
</view>
<view v-else class="flex-row items-center m-l-30rpx" @tap="doLogin">
<text class="sub-text-32">请先登录</text>
<text class="iconfont icon-youjiantou"></text>
</view>
</view>
<view class="m-t-40rpx rounded-20rpx shadow-main p-30rpx p-b-40rpx bg-white!">
<view class="main-text-30 m-b-35rpx">我的服务</view>
<view class="service_item flex-row items-center justify-between" v-for="(v, i) in data.serviceList" :key="i"
@tap="router.jump(v.router)">
<view class="flex-row-center">
<image class="w-40rpx h-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>
<view class="flex-col-center m-t-40rpx m-x-30rpx">
<view class="w-full text-center sub-text-28 p-y-20rpx rounded-40rpx border-solid border border-#0094ff"
@tap="doLogout">
退出登录
</view>
</view>
</crud-layout>
</template>
<style lang="scss" scoped>
.service_item:not(:last-child) {
padding-bottom: 40rpx;
border-bottom: 1px solid #fafafa;
margin-bottom: 40rpx;
}
</style>
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { hooks } from "@@/index"
import { merchantApi } from "@@/api"
const state = reactive<any>({
})
onLoad(() => {
});
onShow(() => {
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, merchantApi.merchantList, null, 'data')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
</script>
<template>
<crud-layout>
<z-paging ref="pageRef" height="100%" :fixed="false" v-model="pageHook.list" :default-page-size="5"
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">
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+80
View File
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { isEmpty } from "radash"
import { hooks } from "@@/index"
import { walletApi } from "@@/api"
import zTabs from "@@/components/other/z-tabs/z-tabs.vue"
const data = {
tabs: ['全部', '收入', '支出'],
}
const state = reactive<any>({
walletId: null,
pa: {
createTime: null,
type: 0,
walletId: null
},
})
onLoad((options: any) => {
if (!isEmpty(options.id)) {
state.walletId = options.id
state.pa.walletId = options.id
}
});
onShow(() => {
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, walletApi.transaction, null, 'data.list')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
const doTab = (e: any) => {
if (state.pa.type != e) {
state.pa.type = e
pageRef.value.reload(true)
}
}
</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="5"
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-30rpx" />
<view class="bg-white rounded-16rpx px-20rpx">
<z-tabs :list="data.tabs" @change="doTab" />
</view>
<view class="h-20rpx" />
<view class="flex-row items-center justify-between card p-x-20rpx p-y-30rpx m-b-20rpx"
v-for="(v, i) in pageHook.list" :key="i">
<view class="flex">
<view class="w-150rpx flex justify-left p-r-25rpx">
<tm-tag v-if="v.title == '更新余额'" size="m">{{ v.title }}</tm-tag>
<tm-tag v-if="v.title == '支付'" color="error" size="m">支付交易</tm-tag>
<tm-tag v-if="v.title == '支付退款'" color="info" size="m">{{ v.title }}</tm-tag>
</view>
<view class="flex-row-center">
<text class="sub-text-30">{{ v.price }}</text>
</view>
</view>
<view class="sub-text-24 text-right">
{{ hooks.useTime(v.createTime, "") }}
</view>
</view>
</z-paging>
</crud-layout>
</template>
<style lang="scss" scoped></style>
+164
View File
@@ -0,0 +1,164 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { isEmpty } from "radash"
import { hooks, router } from "@@/index"
import { merchantApi, walletApi } from "@@/api"
const state = reactive<any>({
showBindDialog: false,
showFreezeDialog: false,
currentCard: {}
})
const bindForm = reactive<any>({
cardNumber: ""
})
const bindFormRules = reactive<any>({
cardNumber: [
{ required: true, message: '请输入卡号' },
{ required: true, min: 8, message: '饭卡长度在8位' },
{ required: true, max: 8, message: '饭卡长度在8位' },
]
})
const freezeForm = reactive<any>({
remark: ""
})
onLoad(() => {
});
onShow(() => {
})
const pageRef = ref<any>(null)
const pageHook = hooks.usePage(pageRef, walletApi.all, null, 'data')
const query = (page: number, pagesize: number) => {
pageHook.startPage(page, pagesize, false, state.pa);
}
const openBindDialog = (v: any) => {
state.showBindDialog = true
state.currentCard = v
}
const closeBindDialog = () => {
state.showBindDialog = false
}
const bindFormRef = ref<any>(null)
const doBind = async () => {
bindFormRef.value.submit()
const { isPass } = bindFormRef.value.validate()
if (!isPass) return
try {
uni.showLoading({ title: '绑定中...', mask: false });
const _pa = {
walletId: state.currentCard.id,
cardNumber: bindForm.cardNumber,
}
const { data, code }: any = await walletApi.bind(_pa)
if (code != 0) throw new Error()
closeBindDialog()
pageRef.value.reload(true)
uni.hideLoading()
uni.showToast({ title: '绑定成功', icon: "none" });
} catch (error) {
console.error(error);
uni.hideLoading()
}
}
const openFreezeDialog = (v: any) => {
state.showFreezeDialog = true
state.currentCard = v
}
const closeFreezeDialog = () => {
state.showFreezeDialog = false
}
const doFreeze = async () => {
try {
uni.showLoading({ title: '申请中...', mask: false });
const _pa = {
walletId: state.currentCard.id,
remark: freezeForm.remark,
}
const { data, code }: any = await walletApi.freeze(_pa)
if (code != 0) throw new Error()
closeFreezeDialog()
pageRef.value.reload(true)
uni.hideLoading()
uni.showToast({ title: '申请成功', icon: "none" });
} catch (error) {
console.error(error);
uni.hideLoading()
}
}
</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="5"
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-30rpx" />
<view v-for="(v, i) in pageHook.list" :key="i" @tap="router.jump('/pages/wallet/detail', { id: v.id })">
<view class="flex-col rounded-20rpx p-x-20rpx p-y-30rpx m-b-30rpx bg-#222222">
<view class="flex-row items-center justify-between">
<view class="flex-row items-center">
<image class="w-52rpx h-52rpx" src="/static/icon/vip.png" mode="aspectFill" />
<text class="m-l-25rpx main-text-32 color-#fbddb0!">
{{ v.walletTypeStr }}
</text>
<text v-if="v.cardNumber" class="m-l-25rpx main-text-32 color-#fbddb0!">
{{ v.cardNumber }}
</text>
</view>
<view class="underline sub-text-28 color-#fbddb0!"
@tap.stop="router.jump('/pages/wallet/recharge', { id: v.id })">
去充值
</view>
</view>
<view class="flex-row items-center justify-between m-t-30rpx">
<text class="main-text-32 color-#fbddb0!">
余额 ¥{{ hooks.useMoney(v.balance) }}
</text>
<text v-if="v.walletTypeCode == 1 && !v.cardNumber"
class="iconfont icon-bangding color-#fbddb0!" @tap.stop="openBindDialog(v)" />
<text v-if="v.walletTypeCode == 1 && v.cardNumber" class="iconfont icon-jiebang color-#fbddb0!"
@tap.stop="openFreezeDialog(v)" />
</view>
</view>
</view>
</z-paging>
</crud-layout>
<tm-drawer z-index="100" v-model:show="state.showBindDialog" :overlayClick="false" title="绑定饭卡" :show-close="true"
position="bottom" size="40%">
<view class="h-50rpx" />
<tm-form ref="bindFormRef" v-model="bindForm" :rules="bindFormRules">
<tm-form-item name="cardNumber" :showLabel="false" :showBottom="false">
<tm-input v-model="bindForm.cardNumber" showClear placeholder="请输入卡号.." />
</tm-form-item>
</tm-form>
<view class="h-50rpx" />
<view class="bg-#0094ff p-y-20rpx text-center rounded-40rpx sub-text-30 color-white!" @tap="doBind">
绑定
</view>
</tm-drawer>
<tm-drawer z-index="100" v-model:show="state.showFreezeDialog" :overlayClick="false" title="申请冻结" :show-close="true"
position="bottom" size="40%">
<view class="h-50rpx" />
<tm-form ref="bindFormRef" v-model="freezeForm">
<tm-form-item name="remark" :showLabel="false" :showBottom="false">
<tm-input v-model="freezeForm.remark" type="textarea" showClear placeholder="请输入备注.." />
</tm-form-item>
</tm-form>
<view class="h-50rpx" />
<view class="bg-#0094ff p-y-20rpx text-center rounded-40rpx sub-text-30 color-white!" @tap="doFreeze">
冻结
</view>
</tm-drawer>
</template>
<style lang="scss" scoped></style>
+172
View File
@@ -0,0 +1,172 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { isEmpty } from "radash"
import { hooks, constant } from "@@/index"
import { walletApi, payApi } from "@@/api"
const data = {
payType: [
{ text: "模拟支付", disabled: false, type: 0, value: "mock" },
{ text: "微信支付", disabled: true, type: 1, value: "wx_pub" },
{ text: "支付宝支付", disabled: true, type: 2, value: "alipay_wap" },
]
}
const state = reactive<any>({
walletId: null,
currentPayType: 0,
currentRecharge: -1,
userInfo: {},
detail: {},
rechargeList: []
})
onLoad((options: any) => {
if (!isEmpty(options.id)) {
state.walletId = options.id
}
});
onShow(() => {
getUserInfo()
getDetail()
getRechargeList()
})
const getDetail = async () => {
const _pa = { walletTypeId: state.walletId }
const { data } = await walletApi.detail(_pa)
state.detail = data
}
const getUserInfo = () => {
const userInfoKey = constant.storageConstant.USERINFO
const userStorage = uni.getStorageSync(userInfoKey)
if (!isEmpty(userStorage)) {
state.userInfo = userStorage
}
}
const getRechargeList = async () => {
const _pa = {}
const { data } = await walletApi.rechargeList(_pa)
state.rechargeList = data
}
const doPay = async () => {
if (state.currentRecharge == -1) {
uni.showToast({ title: '请选择充值金额', icon: "none" });
return
}
try {
uni.showLoading({ title: '充值中...', mask: true });
const targetRecharge = state.rechargeList[state.currentRecharge]
const targetPayType = data.payType[state.currentPayType]
if (isEmpty(targetRecharge)) throw new Error()
const _pa = {
payPrice: targetRecharge.payPrice,
walletId: state.walletId,
packageId: targetRecharge.id,
}
const { data: createData, code: createCode }: any = await walletApi.recharge(_pa)
console.log(createCode, "createCode");
if (createCode != 0) throw new Error()
const _payPa = {
id: createData.payOrderId,
walletId: state.walletId,
channelCode: targetPayType.value,
channelExtras: {},
}
const { data: payData, code: payCode }: any = await payApi.submitOrder(_payPa)
if (payCode != 0) throw new Error()
state.currentPayType = 1
state.currentRecharge = -1
getDetail()
uni.hideLoading()
uni.showToast({ title: '充值成功', icon: "none" });
} catch (error) {
console.error(error);
uni.hideLoading()
uni.showToast({ title: '充值失败', icon: "none" });
}
}
</script>
<template>
<crud-layout title="充值" bg="/static/page/blue-bg.png">
<view class="h-30rpx" />
<view class="flex-row items-center justify-between h-120rpx p-x-30rpx bg-#222222 rounded-20rpx">
<view class="flex-row items-center">
<image class="w-52rpx h-52rpx" src="/static/icon/vip.png" mode="aspectFill" />
<text class="m-l-25rpx main-text-32 color-#fbddb0!">
{{ state.detail.walletTypeStr }}
</text>
</view>
<view class="main-text-28 color-#fbddb0!">
¥{{ hooks.useMoney(state.detail.balance) }}
</view>
</view>
<view class="prefix_title m-t-35rpx">充值</view>
<view class="recharge p-30rpx bg-white rounded-20rpx m-t-20rpx shadow-main">
<view :class="[state.currentRecharge == i ? 'recharge_item_active' : '']" class="recharge_item"
v-for="(v, i) in state.rechargeList" :key="i" @tap="state.currentRecharge = i">
<text class="sub-text-30 text-center">¥{{ hooks.useMoney(v.payPrice) }}</text>
</view>
</view>
<view class="prefix_title m-t-35rpx">支付</view>
<view class="p-30rpx p-b-40rpx bg-white rounded-20rpx m-t-20rpx shadow-main">
<tm-radio-group direction="column" gap="40" v-model="state.currentPayType">
<tm-radio :disabled="v.disabled" v-for="(v, i) in data.payType" :key="i" :value="v.type"
:label="v.text" />
</tm-radio-group>
</view>
<view class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-60rpx sub-text-30 color-white!"
@tap="doPay">
确认充值
</view>
</crud-layout>
</template>
<style lang="scss" scoped>
.prefix_title {
font-size: 28rpx;
font-weight: 800;
color: #333;
&::before {
content: '';
display: inline-block;
width: 8rpx;
height: 24rpx;
background: #0094ff;
border-radius: 4rpx;
margin-right: 10rpx;
}
}
.recharge_item_active {
background: #e9f5fe;
border: 1px solid #e9f5fe !important;
&>text {
color: #0094ff !important;
}
}
.recharge {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 20rpx;
border-bottom: 1px solid #f9f9f9;
&_item {
display: flex;
flex-direction: column;
border: 1px solid #cccccc;
border-radius: 16rpx;
padding: 20rpx 0;
}
}
</style>
+49
View File
@@ -0,0 +1,49 @@
import request from "@/sheep/utils/request";
export default {
foreignList: (params: any) =>
request({
url: "pay/foreign-ticket/get-all",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
foreignItems: (params: any) =>
request({
url: "pay/foreign-ticket/get-items",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
foreignApply: (data: any) =>
request({
url: "pay/foreign-ticket/apply",
method: "POST",
data,
custom: {
showLoading: false,
},
}),
strideList: (params: any) =>
request({
url: "pay/stride-dept/get-all",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
strideApply: (data: any) =>
request({
url: "pay/stride-dept/apply",
method: "POST",
data,
custom: {
showLoading: false,
},
}),
};
+3
View File
@@ -0,0 +1,3 @@
import request from "@/sheep/utils/request";
export default {};
+3
View File
@@ -0,0 +1,3 @@
import request from "@/sheep/utils/request";
export default {};
+17
View File
@@ -0,0 +1,17 @@
export { default as applyApi } from "./apply";
export { default as commonApi } from "./common";
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 userApi } from "./user";
export { default as walletApi } from "./wallet";
+12
View File
@@ -0,0 +1,12 @@
import request from "@/sheep/utils/request";
export default {
merchantList: () =>
request({
url: "/merchant/manage/page-join",
method: "GET",
custom: {
showLoading: false,
},
}),
};
+3
View File
@@ -0,0 +1,3 @@
import request from "@/sheep/utils/request";
export default {};
+20
View File
@@ -0,0 +1,20 @@
import request from "@/sheep/utils/request";
export default {
// 获得支付订单
getOrder: (id, sync) => {
return request({
url: "/pay/order/get",
method: "GET",
params: { id, sync },
});
},
// 提交支付订单
submitOrder: (data) => {
return request({
url: "/pay/order/submit",
method: "POST",
data,
});
},
};
+3
View File
@@ -0,0 +1,3 @@
import request from "@/sheep/utils/request";
export default {};
+33
View File
@@ -0,0 +1,33 @@
import request from "@/sheep/utils/request";
export default {
userInfo: () =>
request({
url: "/sys/user/get",
method: "GET",
custom: {
showLoading: false,
loadingMsg: "登录中",
},
}),
auth: (str: any) =>
request({
url: `/sys/auth/zzdsso/${str}`,
method: "GET",
custom: {
showLoading: false,
loadingMsg: "授权中",
},
}),
refreshToken: (params: any) =>
request({
url: `/sys/auth/refresh-token`,
method: "POST",
params,
custom: {
showLoading: false,
},
}),
};
+84
View File
@@ -0,0 +1,84 @@
import request from "@/sheep/utils/request";
export default {
all: () =>
request({
url: "pay/wallet/get-all",
method: "GET",
custom: {
showLoading: false,
},
}),
detail: (params: any) =>
request({
url: "pay/wallet/detail",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
bind: (data: any) =>
request({
url: "pay/wallet/bind-card",
method: "POST",
data,
custom: {
showLoading: false,
},
}),
freeze: (data: any) =>
request({
url: "pay/wallet/apply-freeze",
method: "POST",
data,
custom: {
showLoading: false,
},
}),
balanceDetail: (params: any) =>
request({
url: "pay/wallet/get-id",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
transaction: (params: any) =>
request({
url: "pay/wallet-transaction/page-by-id",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
summary: (params: any) =>
request({
url: "pay/wallet-transaction/get-summary-by-id",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
rechargeList: (params: any) =>
request({
url: "pay/wallet-recharge-package/list",
method: "GET",
params,
custom: {
showLoading: false,
},
}),
recharge: (data: any) =>
request({
url: "pay/wallet-recharge/create",
method: "POST",
data,
custom: {
showLoading: false,
},
}),
};
@@ -0,0 +1,68 @@
<script setup>
import { defineProps, 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() }" v-if="showLeft && !isTabbar" @tap="doBackClick">
<text class="sicon-back icon" v-if="isHistiry" />
<text class="sicon-home icon" v-else />
</view>
</template>
<style lang="scss" scoped>
.navbar {
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;
z-index: 100;
}
.icon {
font-size: 32rpx;
color: #333;
}
</style>
@@ -0,0 +1,79 @@
<script setup>
import { defineProps, 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()
}
</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>
</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;
}
&_title {
font-size: 30rpx;
font-weight: 500;
}
}
.icon {
font-size: 32rpx;
color: #333;
}
</style>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { defineProps, withDefaults } from "vue";
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";
}
return top + "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,163 @@
<script setup lang="ts">
import { defineProps, withDefaults, watch, ref, reactive, onMounted } from "vue";
import { router } from "@@/index"
export type Props = {
tabBarType: "default" | "just-flot-back"
title: string
scrollHideTabbar: boolean
navBgColor: string
navBgTitleColor: string
mainIsScroll: boolean
bg: string
}
const props = withDefaults(defineProps<Props>(), {
tabBarType: "default",
title: "",
scrollHideTabbar: true,
navBgColor: "transparent",
navBgTitleColor: "#333",
mainIsScroll: false,
bg: "",
});
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;
}
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() {
switch (props.tabBarType) {
case "default":
break;
case "just-flot-back":
state.visble.topSafe = false
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()" />
<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: 1;
}
&_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;
// background: #ecf4f9;
}
&_bottom {}
}
&_foot {}
}
</style>
@@ -0,0 +1,6 @@
// z-tabs全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
export default {
'bg-color': 'transparent',
'active-color': '#1777FF',
'inactive-color': '#999999'
}
@@ -0,0 +1,736 @@
<!-- 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{
font-size: 30rpx;
}
.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>
+3
View File
@@ -0,0 +1,3 @@
export default {
logo: "/static/constant/logo.png"
}
+3
View File
@@ -0,0 +1,3 @@
export { default as imgConstant } from "./img-constant";
export { default as storageConstant } from "./storage-constant";
+4
View File
@@ -0,0 +1,4 @@
export default {
TOKEN: "token",
USERINFO: "user_info",
};
+3
View File
@@ -0,0 +1,3 @@
export * from "./use-page";
export * from "./use-unit";
+78
View File
@@ -0,0 +1,78 @@
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);
console.log(state.list, "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 = {
pageNo: page,
pagesize: pagesize,
...query,
};
try {
const res = await fn(_data);
let _arr = res;
for (let key of struct.split(".")) {
_arr = _arr[key];
}
setPage(_arr);
} catch (e) {
console.log(e);
}
setTimeout((_) => {
if (state.loading == true) {
state.loading = false;
}
}, 500);
};
state.startPage = startPage;
state.scroll = scroll;
return state;
};
+49
View File
@@ -0,0 +1,49 @@
export const useMoney = (num) => {
if (typeof num === "undefined") return "0.00";
const parsedNumber = typeof num === "string" ? parseFloat(num) : num;
return (parsedNumber / 100.0).toFixed(1);
};
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,99 @@
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)})`
}
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import { throttle } from "radash";
function paramsToQuery(params: { [key: string]: string }) {
let query = [];
for (let key in params) {
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;
}
}
+31
View File
@@ -0,0 +1,31 @@
@font-face {
font-family: "iconfont";
/* Project id 4740692 */
src: url('//at.alicdn.com/t/c/font_4740692_ozdkialdak.woff2?t=1731275285379') format('woff2'),
url('//at.alicdn.com/t/c/font_4740692_ozdkialdak.woff?t=1731275285379') format('woff'),
url('//at.alicdn.com/t/c/font_4740692_ozdkialdak.ttf?t=1731275285379') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiangxiajiantouxiao:before {
content: "\e64e";
}
.icon-jiebang:before {
content: "\ee1e";
}
.icon-bangding:before {
content: "\ee1f";
}
.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;
}
+1
View File
@@ -0,0 +1 @@
export * from './request'
+158
View File
@@ -0,0 +1,158 @@
import Request from "luch-request";
import { isEmpty } from "radash";
import { userApi } from "@@/api";
import { router, constant } from "@@/index";
const options = {
// 显示操作成功消息 默认不显示
showSuccess: false,
// 成功提醒 默认使用后端返回值
successMsg: "",
// 显示失败消息 默认显示
showError: true,
// 失败提醒 默认使用后端返回信息
errorMsg: "",
// 显示请求时loading模态框 默认显示
showLoading: true,
// loading提醒文字
loadingMsg: "加载中",
// 需要授权才能请求 默认放开
auth: false,
};
let LoadingInstance = {
target: null,
count: 0,
};
function closeLoading() {
if (LoadingInstance.count > 0) LoadingInstance.count--;
if (LoadingInstance.count === 0) uni.hideLoading();
}
const http = new Request({
baseURL: __APP_SERVICE__,
timeout: 8000,
method: "GET",
header: {
Accept: "*/*",
"Content-Type": "application/json;charset=UTF-8",
Authorization: "",
"tenant-id": "",
},
withCredentials: true,
custom: options,
});
http.interceptors.request.use(
(config) => {
const tokenKey = constant.storageConstant.TOKEN;
const tokenStorage = uni.getStorageSync(tokenKey);
const config_header: any = config?.header;
config_header["tenant-id"] = "1";
if (tokenStorage && tokenStorage.accessToken && config_header) {
config_header.Authorization = tokenStorage.accessToken;
}
if (config.custom && config.custom.showLoading) {
LoadingInstance.count++;
LoadingInstance.count === 1 &&
uni.showLoading({
title: config.custom.loadingMsg,
mask: true,
fail: () => {
uni.hideLoading();
},
});
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
http.interceptors.response.use(
async (response) => {
response.config &&
response.config.custom &&
response.config.custom.showLoading &&
closeLoading();
if (response.data.code !== 0) {
if (
response.config &&
response.config.custom &&
response.config.custom.showError
) {
switch (response.data.code) {
case 401:
setTimeout(() => {
const tokenKey = constant.storageConstant.TOKEN;
const userInfoKey = constant.storageConstant.USERINFO;
uni.removeStorageSync(tokenKey);
uni.removeStorageSync(userInfoKey);
router.jump("/pages/tabbar/my", {}, 2);
}, 500);
break;
case 500:
uni.showToast({
title: response.data.message || "服务器开小差啦,请稍后再试~",
icon: "none",
mask: true,
});
break;
}
return Promise.resolve(response.data);
}
}
if (
response.data.code === 200 &&
response.data.message !== "" &&
response.config &&
response.config.custom &&
response.config.custom.showSuccess
) {
uni.showToast({
title: response.config.custom.successMsg || response.data.message,
icon: "none",
});
}
const tokenKey = constant.storageConstant.TOKEN;
const tokenStorage = uni.getStorageSync(tokenKey);
if (
!isEmpty(tokenStorage) &&
response.config.url != "/sys/auth/refresh-token"
) {
const { refreshToken, expiresTime } = tokenStorage;
const now = new Date().getTime();
const remain = expiresTime - now;
if (remain <= 1000 * 60 * 20) {
const _pa = { refreshToken: refreshToken };
const refreshData = await userApi.refreshToken(_pa);
console.log(refreshData, "refreshData");
if (!isEmpty(refreshData.data)) {
uni.setStorageSync(tokenKey, refreshData.data);
}
}
}
return Promise.resolve(response.data);
},
(error) => {
let errorMessage = "";
if (error !== undefined) {
if (errorMessage) {
uni.showToast({
title: errorMessage || "",
icon: "none",
mask: true,
});
}
}
return false;
}
);
const request = (config: any) => {
return http.middleware(config);
};
export default request;
+6
View File
@@ -0,0 +1,6 @@
export {}
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

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

+60
View File
@@ -0,0 +1,60 @@
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333; // 基本色
$uni-text-color-inverse: #fff; // 反色
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #fff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; // 二级标题颜色
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;
+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,222 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch, PropType } 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: ""
},
position: {
type: String as PropType<"right" | "left" | "bottomLeft" | "bottomRight" | 'top' | 'bottom'>,
default: "right"
},
offset: {
type: Array as PropType<number[]>,
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,516 @@
<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')
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)
}
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
-->
<slot></slot>
</view>
<tm-drawer @open="onOpen" :widthCoverCenter="true" :disabledScroll="true" @close="onClose" @confirm="onConfirm"
@cancel="onCancel" :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>
</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,505 @@
<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, PropType } 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 as PropType<"navigate" | "redirect" | "switchTab" | "reLaunch" | "navigateBack" | "exit">,
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, PropType, 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,113 @@
<script setup lang="ts">
import { ref, watch, provide, onBeforeMount, PropType, 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,757 @@
<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;">
<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%;
padding: 0 24rpx 0px 24rpx;
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, 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>
@@ -0,0 +1,778 @@
<template>
<view class="tmInput" :style="{
width: _width,
height: _height,
minHeight: _minHeight,
color: _fontColor,
}">
<!--
@slot 左侧插槽
-->
<slot name="left">
<text v-if="_attrs.leftText !== ''" :style="{ marginRight: '24rpx', fontSize: _fontSize }">{{
_attrs.leftText
}}</text>
</slot>
<view class="tmInputWrap" :style="[
{
backgroundColor: _color,
fontSize: _fontSize,
minHeight: _minHeight,
borderRadius: _round,
},
_focusHighlight
]" :class="[_attrs.inputClass]">
<!--
@slot 输入框左侧图标插槽
-->
<slot name="leftIcon">
<tm-icon v-if="_attrs.leftIcon" :size="_attrs.fontSize" _style="margin-left:24rpx" :name="leftIcon"
:color="_attrs.iconColor || config.color"></tm-icon>
</slot>
<view class="tmInputWrapBox"
:style="[_attrs.type != 'textarea' ? { height: _minHeight } : { minHeight: _minHeight }]">
<input v-if="_attrs.type != 'textarea'" @keyboardheightchange="onkeyboardheightchange"
@confirm="onconfirm" @focus="onfocus" @blur="onblur" @input="oninput" :value="nowValue"
@change="onchange" :password="_attrs.password && showPasswordEye" :disabled="_attrs.disabled"
:maxlength="_attrs.maxlength" :cursorSpacing="_attrs.cursorSpacing" :focus="_attrs.focus"
:confirmType="_attrs.confirmType" :confirmHld="_attrs.confirmHold" :cursor="_attrs.cursor"
:type="_attrs.type" :cursorColor="_currorColor" :selectionStart="_attrs.selectionStart"
:selectionEnd="_attrs.selectionEnd" :adjustPosition="_attrs.adjustPosition"
:autoBlur="_attrs.autoBlur" :ignoreCompositionEvent="_attrs.ignoreCompositionEvent"
:alwaysEmbed="_attrs.alwaysEmbed" :holdKeyboard="_attrs.holdKeyboard"
:safePasswordCertPath="_attrs.safePasswordCertPath" :safePasswordLength="_attrs.safePasswordLength"
:safePasswordTimeStamp="_attrs.safePasswordTimeStamp" :safePasswordNonce="_attrs.safePasswordNonce"
:safePasswordSalt="_attrs.safePasswordSalt" :safePasswordCustomHash="_attrs.safePasswordCustomHash"
:randomNumber="_attrs.randomNumber" :controlled="_attrs.controlled"
:alwaysSystem="_attrs.alwaysSystem" :inputmode="_attrs.inputmode" class="tmInputInput tmInputText"
:placeholderStyle="`textAlign: ${_attrs.align};fontSize:${_fontSize};` + _attrs.placeholderStyle"
:placeholder="_attrs.placeholder" :placeholderClass="_attrs.placeholderClass" :style="[
{
textAlign: _attrs.align,
padding: `0px ${_inputPadding}`,
color: _inputFontColor,
},
_attrs.inputStyle
]" />
<!-- @vue-ignore -->
<textarea v-else @keyboardheightchange="onkeyboardheightchange" @confirm="onconfirm" @focus="onfocus"
@blur="onblur" @input="oninput" @linechange="onlinechange" :autoHeight="_attrs.autoHeight"
:fixed="_attrs.fixed" :showConfirmBar="_attrs.showConfirmBar" :value="nowValue"
:disabled="_attrs.disabled" :maxlength="_attrs.maxlength" :cursorSpacing="_attrs.cursorSpacing"
:focus="_attrs.focus" :confirmHld="_attrs.confirmHold" :cursor="_attrs.cursor"
:cursorColor="_currorColor" :selectionStart="_attrs.selectionStart"
:selectionEnd="_attrs.selectionEnd" :adjustPosition="_attrs.adjustPosition"
:autoBlur="_attrs.autoBlur" :ignoreCompositionEvent="_attrs.ignoreCompositionEvent"
:holdKeyboard="_attrs.holdKeyboard" :inputmode="_attrs.inputmode"
:placeholderStyle="`textAlign: ${_attrs.align};fontSize:${_fontSize};` + _attrs.placeholderStyle"
:placeholder="_attrs.placeholder" :placeholderClass="_attrs.placeholderClass"
class="tmInputInput tmInputTextArea" :style="[
{
width: 'auto',
textAlign: _attrs.align,
padding: `${_inputPadding}`,
color: _inputFontColor,
},
_attrs.autoHeight ? { minHeight: _attrs.autoHeight } : { height: _minHeight },
_attrs.inputStyle
]">
</textarea>
<!-- <view class="tmInputPlaceholder" :class="[showPlaceholder&&!isFocus?'tmInputPlaceholderOn':'tmInputPlaceholderOff',_attrs.placeholderClass]" :style="[
{
textAlign: _attrs.align,
fontSize: _fontSize,
lineHeight: _minHeight,
padding: `0px ${_inputPadding}`,
},
_attrs.placeholderStyle,
]">
{{ _attrs.placeholder }}
</view> -->
<view class="tmInputShowChar" v-if="_attrs.maxlength > -1 && _attrs.showCharCount">
<!--
@slot 输入框右侧显示字数
-->
<slot name="maxlength">
{{ String(nowValue).length + '/' + _attrs.maxlength }}
</slot>
</view>
</view>
<tm-icon :size="_attrs.fontSize" @click="onclear" _style="padding:16rpx;" :color="_clearColor"
v-if="_attrs.showClear" name="close-circle-fill"></tm-icon>
<tm-icon v-if="_attrs.showEye" :size="_attrs.fontSize" @click="showPasswordEye = !showPasswordEye"
_style="padding:16rpx;" :color="showPasswordEye ? _clearColor : config.color"
:name="showPasswordEye ? 'eye-off-line' : 'eye-line'"></tm-icon>
<!--
@slot 输入框内右侧图标
-->
<slot name="rightIcon">
<tm-icon :size="_attrs.fontSize" v-if="_attrs.rightIcon"
:_style="_attrs.showClear ? 'margin-right:24rpx' : 'margin:0 24rpx'" :name="rightIcon"
:color="_attrs.iconColor || config.color"></tm-icon>
</slot>
</view>
<!--
@slot 输入框右侧
-->
<slot name="right">
<text v-if="_attrs.rightText != ''" :style="{ marginLeft: '24rpx', fontSize: _fontSize }">{{
_attrs.rightText
}}</text>
</slot>
</view>
</template>
<script setup lang="ts">
import { type ComputedRef, type PropType, computed, nextTick, ref, watchEffect } from "vue";
import {
arrayNumberValid,
arrayNumberValidByStyleMP,
covetUniNumber,
linearValid,
} from "../../libs/tool";
import { useTmConfig } from "../../libs/config";
import {
getDefaultColor,
setTextColorLightByDark
} from "../../libs/colors";
/**
* @displayName 输入框
* @exportName tm-input
* @category 表单组件
* @description 可以全局配置圆角风格.
* @constant 平台兼容
* | H5 | uniAPP | 小程序 | version |
| --- | --- | --- | --- |
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
*/
defineOptions({ name: "TmInput" });
const { config } = useTmConfig();
const emits = defineEmits([
/**
* 输入框行数变化时触发(文本哉才有)
* @property {object} evt {height: 0, heightRpx: 0, lineCount: 0}
*/
'linechange',
/**
* 键盘高度变化时触发
* @property {number} height 键盘高度
*/
'keyboardheightchange',
/**
* 点击完成按钮时触发
* @property {string} value 输入的值
*/
'confirm',
/**
* 失去焦点时触发
*/
'blur',
/**
* 获取焦点时触发
*/
'focus',
/**
* 清除事件
*/
'clear',
/**
* 输入时候触发
* @property {string} value 输入的值
*/
"input",
/**
* 等同v-model
* @property {string} value 输入的值
*/
"update:modelValue",
]);
const attrs = defineProps({
/**
* 输入框的值
*/
modelValue: {
type: [String, Number],
default: "",
},
/**
* 输入框自定义的style样式
*/
inputStyle: {
type: String,
default: ""
},
/**
* 输入框自定义的class样式
*/
inputClass: {
type: String,
default: ""
},
/**
* 输入类型,可选值:text,number,idcard,digit,tel,safe-password,nickname,
* textarea:文本域
*/
type: {
type: String as PropType<
"text" | "number" | "idcard" | "digit" | "tel" | "safe-password" | "nickname" | "textarea"
>,
deffault: "",
},
/**
* 是否是密码框
*/
password: {
type: Boolean,
default: false,
},
/**
* 显示密码小眼睛
*/
showEye: {
type: Boolean,
default: false,
},
/**
* 输入框的占位文字
*/
placeholder: {
type: String,
default: "请输入或选择",
},
/**
* 输入框的占位文字样式
*/
placeholderStyle: {
type: String,
default: "color: #b1b1b1;",
},
/**
* 输入框的占位样式
*/
placeholderClass: {
type: String,
default: "",
},
/**
* 输入框的对齐方式,可选值:left,right,center
*/
align: {
type: String as PropType<"left" | "right" | "center">,
default: "left",
},
/**
* 输入框背景
*/
color: {
type: String,
default: "",
},
/**
* 输入框暗黑背景,空值取全局的配置
* 提供会覆盖全局的配色。默认是透明
*/
darkBgColor: {
type: String,
default: "transparent",
},
/**
* 输入框字体大小
*/
fontSize: {
type: String,
default: "32",
},
width: {
type: String,
default: "auto",
},
height: {
type: String,
default: "auto",
},
minHeight: {
type: String,
default: "88",
},
/**
* 输入框内的左右间隙
*/
inputPadding: {
type: String,
default: "24"
},
/**
* 输入框圆角,默认空值取全局的配置
*/
round: {
type: [String, Number, Array<string | number>],
default: ""
},
/**
* 是否显示清除按钮
*/
showClear: {
type: Boolean,
default: false
},
/**
* 清除按钮颜色
*/
clearColor: {
type: String,
default: "#b1b1b1"
},
/**
* 是否显示字符字统计
*/
showCharCount: {
type: Boolean,
default: false
},
/**
* 字体颜色
*/
fontColor: {
type: String,
default: "#333333"
},
/**
* 暗黑模式字体颜色,如果不填写自动反转
*/
darkFontColor: {
type: String,
default: ""
},
/**
* 激活时的字体颜色
* 空值取全局主题色,并且darkFontColor会失效.
* 如果填写none则不生效
*/
focusFontColor: {
type: String,
default: ""
},
/**
* 左图标的名称
*/
leftIcon: {
type: String,
default: ""
},
/**
* 左图标的颜色
* 默认空值取全局的主题色。
*/
iconColor: {
type: String,
default: ""
},
/**
* 右图标的名称
*/
rightIcon: {
type: String,
default: ""
},
/**
* 右文字
*/
rightText: {
type: String,
default: ""
},
/**
* 右文字
*/
leftText: {
type: String,
default: ""
},
/**
* 是否禁用
*/
disabled: {
type: Boolean,
default: false
},
/**
* 是否聚焦
*/
focus: {
type: Boolean,
default: false
},
/**
* 键盘右下解按钮文字,可以取值:send,search,next,done
* 只对input有效,对textarea无效
*/
confirmType: {
type: String as PropType<"send" | "search" | "next" | "done">,
default: "done"
},
/**
* 光标与键盘的距离,单位px
*/
cursorSpacing: {
type: Number,
default: 0
},
/**
* 输入框的最大长度
*/
maxlength: {
type: Number,
default: -1
},
confirmHold: {
type: Boolean,
default: false
},
cursor: {
type: Number,
default: 0
},
/**
* 光标颜色,默认空值取全局配置
*/
cursorColor: {
type: String,
default: ""
},
/**
* 光标起始位置
*/
selectionStart: {
type: Number,
default: -1
},
/**
* 光标结束位置
*/
selectionEnd: {
type: Number,
default: -1
},
/**
* 是否自动聚焦
*/
adjustPosition: {
type: Boolean,
default: true
},
/**
* 是否自动失焦
*/
autoBlur: {
type: Boolean,
default: false
},
ignoreCompositionEvent: {
type: Boolean,
default: true
},
/**
* 强制 input 处于同层状态
*/
alwaysEmbed: {
type: Boolean,
default: true
},
/**
* focus时,点击页面的时候不收起键盘
*/
holdKeyboard: {
type: Boolean,
default: false
},
safePasswordCertPath: {
type: String,
default: "",
},
safePasswordLength: {
type: Number,
default: NaN
},
safePasswordTimeStamp: {
type: Number,
default: NaN
},
safePasswordNonce: {
type: String,
default: ''
},
safePasswordSalt: {
type: String,
default: ''
},
safePasswordCustomHash: {
type: String,
default: ''
},
randomNumber: {
type: Boolean,
default: false
},
controlled: {
type: Boolean,
default: false
},
alwaysSystem: {
type: Boolean,
default: false
},
/**
* input 输入模式
*/
inputmode: {
type: String as PropType<"url" | "email" | "search" | "tel" | "text" | "numeric" | "none" | "decimal">,
default: "text"
},
autoHeight: {
type: Boolean,
default: false
},
fixed: {
type: Boolean,
default: false
},
showConfirmBar: {
type: Boolean,
default: false
},
/**
* 是否高亮聚焦当前输入框
* 会使用全局的主题色为边框色。
*/
focusHighlight: {
type: [Boolean, String],
default: true
},
/**
* 高亮聚焦当前输入框的样式
* 默认为自动全局2px solid 主题色,如果填写了以你的为准。
*/
focusHighlightStyle: {
type: String,
default: ''
},
});
const _attrs = computed(() => attrs);
const nowValue = ref<string | number>("");
const isFocus = ref<boolean>(false);
const showPlaceholder = computed(() => {
return nowValue.value == "" ? true : false;
});
const showPasswordEye = ref(_attrs.value.password)
const _color = computed(() => {
let color = attrs.color;
if (color == "") {
color = config.inputLightColor
}
if (config.mode == "dark") {
color = attrs.darkBgColor;
if (color == "") {
color = config.inputDarkColor;
}
}
return getDefaultColor(color);
});
const _focusHighlight = computed(() => {
let defaultsolid = 'border:solid 2px transparent;' + _attrs.value.inputStyle
if (!isFocus.value) return defaultsolid;
return attrs.focusHighlight ? (attrs.focusHighlightStyle || `border:solid 2px ${getDefaultColor(config.color)} !important`) : defaultsolid;
})
const _fontColor = computed(() => {
let fontColor = attrs.fontColor;
if (config.mode == "dark") {
fontColor = attrs.darkFontColor;
if (fontColor == "") {
fontColor = setTextColorLightByDark(attrs.fontColor)
}
}
return getDefaultColor(fontColor)
})
const _currorColor = computed(() => {
return getDefaultColor(_attrs.value.cursorColor || config.color)
})
const _inputFontColor = computed(() => {
if (!isFocus.value) return _fontColor.value
if (attrs.fontColor == 'none') return _fontColor.value;
return getDefaultColor(attrs.focusFontColor || config.color)
})
const _fontSize = computed(() => {
return covetUniNumber(attrs.fontSize, config.unit);
});
const _width = computed(() => {
return covetUniNumber(attrs.width, config.unit);
});
const _height = computed(() => {
return covetUniNumber(attrs.height, config.unit);
});
const _minHeight = computed(() => {
return covetUniNumber(attrs.minHeight, config.unit);
});
const _inputPadding = computed(() => {
return covetUniNumber(attrs.inputPadding, config.unit);
});
const _round = computed((): string | undefined => {
let round = attrs.round;
if (round === '' && config.inputRadius !== '') {
round = config.inputRadius;
}
if (round === '') return undefined
return arrayNumberValid(round, '0px').join(' ')
});
const _clearColor = computed(() => getDefaultColor(attrs.clearColor))
const oninput = (evt: Event) => {
// @ts-ignore
let value: string | number = evt.detail.value;
let pvalue = value
if (attrs.type == "number" || attrs.type == "digit" || attrs.type == "tel") {
pvalue = typeof pvalue == "number" ? pvalue.toString() : pvalue;
try {
let oladvalue = parseFloat(pvalue);
if (isNaN(oladvalue) || /[^0-9.]+/.test(pvalue)) {
pvalue = ''
}
} catch (_) {
}
}
nowValue.value = value;
nextTick(() => {
nowValue.value = pvalue;
emits("input", pvalue);
emits("update:modelValue", pvalue);
})
return value
};
const onclear = () => {
nowValue.value = '';
emits('update:modelValue', '')
emits("clear")
}
const onfocus = () => {
isFocus.value = true;
};
const onblur = () => {
isFocus.value = false;
};
const onconfirm = (evt: any) => {
emits("confirm", evt.detail.value);
};
const onkeyboardheightchange = (evt: any) => {
emits('keyboardheightchange', evt.detail.height);
};
const onlinechange = (evt: any) => {
emits('linechange', evt.detail);
}
const onchange = (evt: any) => {
// 用来监测微信输入昵称的值。
let val = evt.detail.value;
if (_attrs.value.type == 'nickname' && val != nowValue.value && val) {
nowValue.value = val;
emits("input", val);
emits("update:modelValue", val);
}
}
watchEffect(() => {
// if(nowValue.value==attrs.modelValue) return;
nowValue.value = attrs.modelValue;
});
</script>
<script lang="ts">
export default {
options: {
styleIsolation: "apply-shared",
virtualHost: true,
addGlobalClass: true,
multipleSlots: true,
},
};
</script>
<style lang="scss" scoped>
.tmInputShowChar {
font-size: 12px;
position: absolute;
right: 24rpx;
bottom: 16rpx;
pointer-events: none;
opacity: 0.5;
}
.tmInput {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
overflow: hidden;
box-sizing: border-box;
box-sizing: border-box;
}
.tmInputWrap {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
overflow: hidden;
flex: 1;
box-sizing: border-box;
transition-property: border-color;
transition-duration: 0.3s;
transition-timing-function: linear;
border-color: transparent;
}
.tmInputWrapBox {
position: relative;
height: 100%;
flex: 1;
box-sizing: border-box;
.tmInputInput {
height: 100%;
}
.tmInputPlaceholder {
box-sizing: border-box;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
text-overflow: ellipsis;
line-clamp: 1;
-webkit-box-orient: vertical;
display: -webkit-box;
white-space: inherit;
text-wrap: nowrap;
opacity: 0;
transition: all 0.2s cubic-bezier(.42, .38, .15, .93);
&.tmInputPlaceholderOn {
opacity: 1;
transform: translateY(0%);
}
&.tmInputPlaceholderOff {
opacity: 0;
transform: translateY(50rpx);
}
}
}
</style>
@@ -0,0 +1,601 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, 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-modal
* @category 反馈组件
* @description 可全局统一更改风格。使用时注意下:如果height:auto,将失去内容滚动效果。如果要内容滚动height必定是一个值。
* @constant 平台兼容
* | H5 | uniAPP | 小程序 | version |
| --- | --- | --- | --- |
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
*/
defineOptions({ name: 'TmModal' });
const { config } = useTmConfig()
const props = defineProps({
/**
* 自定义遮罩样式
*/
customStyle: {
type: String,
default: ""
},
/**
* 标题
*/
title: {
type: String,
default: "标题"
},
/**
* 显示底部操作栏
*/
showFooter: {
type: Boolean,
default: true
},
/**
* 是否显示底部关闭按钮
*/
showTitle: {
type: Boolean,
default: true
},
/**
* 是否显示底部关闭按钮
*/
showClose: {
type: Boolean,
default: false
},
/**
* 显示取消按钮
*/
showCancel: {
type: Boolean,
default: true
},
/**
* 遮罩是否允许点击被关闭
*/
overlayClick: {
type: Boolean,
default: true
},
/**
* 显示可v-model:show双向绑定
*/
show: {
type: Boolean,
default: false
},
/**
* 动画时间
*/
duration: {
type: Number,
default: 250
},
/**
* 取消按钮的文本
*/
cancelText: {
type: String,
default: "取消"
},
/**
* 确认按钮的文本
*/
confirmText: {
type: String,
default: "确认"
},
/**
* 打开方向为上和下时的圆角
* 空值时,取全局配置的圆角。
*/
round: {
type: [String, Number],
default: ""
},
/**
* 宽,百分比,Px,rpx,auto都支持
*/
width: {
type: [String, Number],
default: "84%"
},
/**
* 宽,百分比,Px,rpx,auto都支持
*/
height: {
type: [String, Number],
default: "350"
},
/**
* 可以是百分比,px,rpx单位数字。如果你不带单位,默认转换为rpx单位。
*/
maxHeight: {
type: [String, Number],
default: "80%"
},
/**
* 是否禁用内部的scroll标签
* 禁用后内容不会滚动,如果设定了指定高,内容超出指定高,会被裁切
* 但如果没有指定高,内容自动的话,高是自动的。
* 有这个属性是因为截止4.03scroll-view里面放input不会上推键盘,及内部的view touchMove会失效。
*/
disabledScroll: {
type: Boolean,
default: false
},
/**
* 容器背景色
*/
bgColor: {
type: String,
default: "white"
},
/**
* 暗黑时的容器背景色,不填写的话取sheetDarkColor
*/
darkBgColor: {
type: String,
default: ""
},
zIndex: {
type: [String, Number],
default: "1105"
},
/**
* 内容区域的间隙
*/
contentPadding: {
type: [String, Number],
default: "32"
}
})
const emit = defineEmits([
/**
* 点击遮罩事件
*/
'click',
/**
* 关闭是触发
*/
'close',
/**
* 打开时触发
*/
'open',
/**
* 打开前执行
*/
'beforeOpen',
/**
* 关闭前执行
*/
'beforeClose',
/**
* 等同v-model:show
*/
'update:show',
/**
* 取消时触发
*/
'cancel',
/**
* 确认时触发
*/
'confirm'
])
const _width = ref(0)
const _height = ref(0)
const showOverflay = ref(false)
const actinon = ref(false)
const status = ref("")
const id = ref("tmModal" + getUid())
const wrapId = ref("tmModal" + getUid())
const tid = ref(0)
const tid2 = ref(0)
const windtop = ref(0)
const tantiaoTrue = ref(false)
const _customStyle = computed(() => props.customStyle)
const _show = computed(() => props.show)
const _showClose = computed(() => props.showClose)
const _duration = computed(() => props.duration)
const _title = computed(() => props.title)
const _showTitle = computed(() => props.showTitle)
const _round = computed(() => {
if (props.round == "") {
return covetUniNumber(config.modalRadius, config.unit)
}
return covetUniNumber(props.round, config.unit)
})
const _contentHeight = ref('auto')
const proxy = getCurrentInstance()?.proxy;
const _c_width = computed(() => covetUniNumber(props.width, config.unit))
const _c_height = computed(() => covetUniNumber(props.height, config.unit))
const _showFooter = computed(() => props.showFooter)
const _maxHeight = computed(() => covetUniNumber(props.maxHeight, config.unit))
const _contentPadding = computed(() => covetUniNumber(props.contentPadding, config.unit))
const _showCancel = computed(() => props.showCancel)
const _cancelText = computed(() => props.cancelText)
const _confirmText = computed(() => props.confirmText)
const _animationFun = computed(() => config.animation)
const __height = computed(() => {
let h = '100%'
// #ifdef WEB
h = `calc(100% - ${windtop.value}px)`
// #endif
return h
})
const _bgColor = computed(() => {
if (config.mode == 'dark') {
if (props.darkBgColor != '') return getDefaultColor(props.darkBgColor)
return getDefaultColor(config.sheetDarkColor)
}
return getDefaultColor(props.bgColor)
})
watch(() => props.show, (newVal) => {
if (newVal) {
showAlert()
} else {
closeAlert()
}
})
onMounted(() => {
let sys = uni.getWindowInfo()
// #ifndef APP
_width.value = sys.windowWidth
_height.value = sys.windowHeight - sys.windowTop
windtop.value = sys.windowTop
// #endif
// #ifdef APP
_width.value = sys.windowWidth
_height.value = sys.windowHeight
// #endif
if (_show.value) {
showAlert()
}
})
onBeforeUnmount(() => {
clearTimeout(tid.value)
clearTimeout(tid2.value)
})
const overflayMoveTouch = (evt: TouchEvent) => {
evt.preventDefault()
}
const cancelEvt = () => {
/**
* 取消时触发
*/
emit('cancel')
closeAlert()
}
const confirmEvt = () => {
/**
* 确认时触发
*/
emit('confirm')
closeAlert()
}
const onClickOverflowy = (evt: Event) => {
evt.stopPropagation()
/**
* 点击遮罩事件
*/
emit("click")
if (!props.overlayClick){
clearTimeout(tid2.value)
tantiaoTrue.value = true
tid2.value = setTimeout(() => {
tantiaoTrue.value = false
}, 200);
return;
}
closeAlert()
}
const closeAlert = () => {
if (actinon.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)
}
const openDrawer = () => {
showAlert()
}
const onEnd = () => {
actinon.value = false
if (status.value == 'close') {
showOverflay.value = false
/**
* 关闭时执行
*/
emit('close')
/**
* 等同v-model:show
*/
emit('update:show', false)
} else {
/**
* 打开执行的事件
*/
emit('open')
}
}
const maskerMove = (evt: TouchEvent) => {
evt.stopPropagation()
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 标签触发显示遮罩,免于使用变量控制
@binding {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" ref="tmModalWrap" v-if="showOverflay"
:id="id" class="tmModalWrap tmModalWrap_center"
:class="[status == 'open' ? 'tmModalWrap_on' : 'tmModalWrap_off']"
:style="[{ top: windtop + 'px', zIndex: zIndex, width: '100%', height: __height, 'transition-timing-function': _animationFun }, _customStyle]">
<view @click.stop="" ref="tmModalWrapContent" class="tmModalWrapContent "
:class="[status == 'open' ? 'tmModalWrapContent_on' : 'tmModalWrapContent_off',tantiaoTrue?'tmModalWrapContent_Tantiao':'']" :id="wrapId"
:style="{
width: _c_width,
height: _c_height,
maxWidth: '750px',
borderRadius: _round,
maxHeight: _maxHeight != '' ? _maxHeight : '100%',
backgroundColor: _bgColor,
transitionDuration: _duration + 'ms',
'transition-timing-function': _animationFun
}">
<view ref="tmModalWrapBox" class="tmModalWrapBox" :style="{ borderRadius: _round }">
<view class="tmModalXclose">
<tm-icon v-if="_showClose" @click="closeAlert" color="#dcdcdc" size="52"
name="close-circle-fill"></tm-icon>
</view>
<view>
<view v-if="_showTitle" class="tmModalTitleBox">
<!--
@slot 标题插槽
@binding {Boolean} show - 当前是否已显示
-->
<slot name="title" :show="show">
<tm-text font-size="34" class="tmModaltitleBoxTitle">{{ _title }}</tm-text>
</slot>
</view>
</view>
<view v-if="!disabledScroll&&_c_height!='auto'" style="flex: 1;" class="tmModalWrapBoxContent" >
<scroll-view style="height:100%;position:absolute;width:100%" :scroll-y="true"
:rebound="false">
<view :style="{ padding: `0px ${_contentPadding} 0px ${_contentPadding}` }">
<!--
@slot 默认插槽
-->
<slot></slot>
</view>
</scroll-view>
</view>
<view v-else :style="{ height: '100%', padding: _contentPadding }">
<!--
@slot 默认插槽
-->
<slot></slot>
</view>
<view v-if="showFooter" class="tmModalFooter" :style="{ backgroundColor: _bgColor }">
<!--
@slot 底部操作栏
-->
<slot name="footer">
<tm-button @click="cancelEvt" v-if="_showCancel" skin="thin"
style="margin-right: 16px;flex:1">{{ _cancelText }}</tm-button>
<tm-button @click="confirmEvt" style="flex:1">{{ _confirmText }}</tm-button>
</slot>
</view>
</view>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
</root-portal>
<!-- #endif -->
<!-- #ifdef H5 -->
</teleport>
<!-- #endif -->
</view>
</template>
<style lang="scss" scoped>
.tmModalWrapBox {
display: flex;
flex-direction: column;
/* background-color: white; */
height: 100%;
width: 100%;
}
.tmModalFooter {
display: flex;
width: 100%;
/* background-color: white; */
padding: 24rpx;
flex-direction: row;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
}
.tmModalXclose {
position: absolute;
right: 24rpx;
top: 18rpx;
z-index: 100;
}
.tmModaltitleBoxTitle {
font-size: 32rpx;
}
.tmModalTitleBox {
height: 100rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-weight: bold;
}
.tmModaltitleBox {
max-width: 350px;
overflow: hidden;
lines: 1;
text-overflow: ellipsis;
font-size: 14px;
}
.tmModalWrap_center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.tmModalWrapContent {
transition-property: transform, opacity;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tmModalWrapContent_on {
transform: scale(1);
opacity: 1;
}
.tmModalWrapContent_off {
transform: scale(0.54);
opacity: 0;
}
.tmModalWrapBoxContent{
position: relative;
}
.tmModalWrap {
opacity: 1;
position: fixed;
left: 0;
top: 0px;
transition-duration: 350ms;
transition-property: background-color;
}
.tmModalWrap_on {
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(3px);
}
.tmModalWrap_off {
background-color: rgba(0, 0, 0, 0);
}
@keyframes tantiao {
0%{
transform: scale(1);
}
60%{
transform: scale(1.05);
}
75%{
transform: scale(0.95);
}
85%{
transform: scale(1.05);
}
100%{
transform: scale(1);
}
}
.tmModalWrapContent_Tantiao{
animation: tantiao 0.15s linear;
}
</style>
@@ -0,0 +1,419 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, ComponentInstance, onUpdated, nextTick, VueElement, inject, watchEffect } from 'vue';
import { arrayNumberValid, arrayNumberValidByStyleMP, covetUniNumber, arrayNumberValidByStyleBorderColor, linearValid, getUnit, getUid } from "../../libs/tool";
import { getDefaultColor, getDefaultColorObj, getOutlineColorObj, getTextColorObj, getThinColorObj, isBlackAndWhite, setBgColorLightByDark } from "../../libs/colors";
import { useTmConfig } from "../../libs/config";
import { onPageScroll } from '@dcloudio/uni-app';
/**
* @displayName 标题导航
* @exportName tm-navbar
* @category 导航组件
* @description 标题导航,用途页面头部标题。可以默认透明,滚动实现背景可变的标题导航。
* @constant 平台兼容
* | H5 | uniAPP | 小程序 | version |
| --- | --- | --- | --- |
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
*/
defineOptions({ name: 'TmNavbar' });
const { config } = useTmConfig()
const emits = defineEmits([
/**
* 当滚动页面时,导航栏位置
* 与滚动之间的距离差比(0~1)
* 0表示在顶部,1表示已经超过了导航高
* 主要是用来类型滚动时设置导航顶部的一些布局变化。
*/
'fiexdChange',
/**
* 初始化完成向下发送一个事件用于告知本组件实际的高
* @param {number} height -单位px,组件高。
*/
'init'
])
const props = defineProps({
/**
* 是否底部占位内容,如果为false底部悬空。你的页面内容将自动压在导航栏的底部。
*/
isPlace: {
type: Boolean,
default: true
},
/**
* 背景颜色,注意这是静态时的背景色。
*/
bgColor: {
type: String,
default: 'white'
},
/**
* 暗黑的背景颜色。
*/
darkBgColor: {
type: String,
default: "#000000"
},
/**
* 背景颜色,这是滑动时超过指定本状态栏高度时自动渐变到此颜色
* 如果为空时,不会有动态背景
* 如果提供的是白或者黑,暗黑时自动取反。
*/
activeBgColor: {
type: String,
default: ''
},
/**
* 返回按钮的颜色.默认是取titleColor,如果你单独定义了
* 以你定义的为准
*/
backColor: {
type: String,
default: ''
},
/**
* 标题
*/
title: {
type: String,
default: '标题'
},
/**
* 默认标题颜色,暗黑是取白,如果有其它需求建议插槽。
*/
titleColor: {
type: String,
default: '#333333'
},
/**
* 动态悬浮时标题颜色,如果为暗黑时,你提供的颜色为白或者黑会反色
* 如果提供的是彩色自动加深或者提亮
*/
titleActiveColor: {
type: String,
default: '#333333'
},
/**
* 标题文字大小
*/
titleFontSize: {
type: String,
default: '34',
},
/**
* 右边的宽度。
*/
lrWidth: {
type: String,
default: '100',
},
/**
* 左边的宽度。
*/
llWidth: {
type: String,
default: '100',
},
/**
* 层级。
*/
zIndex: {
type: Number,
default: 90,
},
/**
* 左边按钮默认点击返回上页。
* 但如果上页返回失败(通常见于直接程序启动本面,无法进行上页返回时)
* 失败后返回的页面。默认是首页。
*/
backErrorPath: {
type: String,
default: '/pages/index/index'
},
/**
* 是否显示返回按钮。
*/
showNavBack: {
type: Boolean,
default: true
},
/**
* 渐变背景,如果提供,上面的BgColor背景和暗黑背景将失效。
* 例:['to right','#ff667f','#ff5416']
*/
linearGradient: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
/**
* 动态悬浮时的渐变背景,
* 提供后上面的 activeBgColor的背景和暗黑背景将失效。
* 例:['to right','#ff667f','#ff5416']
*/
linearActiveGradient: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
/**
* 静态在顶部时是否透明背景
*/
staticTransparent: {
type: Boolean,
default: false
},
/**
* 如果需要悬浮滚动请在外部页面中监听onPageScroll事件,并把值传到这里。
*/
scrollTop:{
type:Number,
default:0
}
})
const statusBarHeight = ref(44)
const navbarHeight = 50
const isFiexd = ref(false)
const fiexRatio = ref(1)
const _stop = computed(()=>props.scrollTop)
const _linearGradient = computed((): string => {
if (props.linearGradient.length < 3) return '';
let str = props.linearGradient.join(",");
return `linear-gradient(${str})`
})
const _linearActiveGradient = computed((): string => {
if (props.linearActiveGradient.length < 3) return '';
let str = props.linearActiveGradient.join(",");
return `linear-gradient(${str})`
})
const _lrWidth = computed((): string => covetUniNumber(props.lrWidth, config.unit))
const _llWidth = computed((): string => covetUniNumber(props.llWidth, config.unit))
const _titleFontSize = computed((): string => covetUniNumber(props.titleFontSize, config.unit))
const _bgColor = computed((): string => {
let bgcolor = props.bgColor
if (config.mode == 'dark') {
bgcolor = props.darkBgColor == "" ? "#000000" : props.darkBgColor
}
return getDefaultColor(bgcolor)
})
const _activeBgColor = computed((): string => {
let color = _bgColor.value
if (props.activeBgColor != "") {
color = getDefaultColor(props.activeBgColor)
}
let isba = isBlackAndWhite(props.titleActiveColor)
if (isba) {
if (config.mode == 'dark') {
color = setBgColorLightByDark(color)
}
}
return color
})
const _titleColor = computed((): string => {
if (config.mode == 'dark') return "#ffffff"
return getDefaultColor(props.titleColor)
})
const _backColor = computed((): string => {
if (props.backColor != '') return getDefaultColor(props.backColor)
return _titleColor.value
})
const _titleActiveColor = computed((): string => {
let isba = isBlackAndWhite(props.titleActiveColor)
if (isba) {
if (config.mode == 'dark') return "#ffffff"
}
return getDefaultColor(props.titleActiveColor)
})
const _title = computed((): string => props.title)
const _styleMap = computed(() => {
let maps: Record<string, string | number> = {};
if (props.staticTransparent) {
maps['opacity'] = fiexRatio.value
} else {
maps['opacity'] = '1'
}
maps['height'] = (statusBarHeight.value + navbarHeight) + 'px'
if (isFiexd.value) {
if (_linearActiveGradient.value != '') {
maps['background-image'] = _linearActiveGradient.value
} else if (_activeBgColor.value != '') {
maps['background-color'] = _activeBgColor.value
} else {
maps['background-color'] = _bgColor.value
}
} else {
if (_linearGradient.value != '') {
maps['background-image'] = _linearGradient.value
} else {
maps['background-color'] = _bgColor.value
}
}
return maps
})
onMounted(() => {
let sys = uni.getSystemInfoSync();
statusBarHeight.value = sys.statusBarHeight || 0
emits('init', statusBarHeight.value + navbarHeight)
if (props.staticTransparent) {
fiexRatio.value = 0
}
})
watchEffect(() => {
let barheight = statusBarHeight.value + navbarHeight;
isFiexd.value = _stop.value > 0
if (props.staticTransparent) {
fiexRatio.value = Math.max(Math.min(_stop.value / barheight, 1), 0)
}
emits("fiexdChange", fiexRatio.value)
})
const backGo = () => {
// #ifdef WEB
window.history.back()
// #endif
// #ifndef WEB
uni.navigateBack({
fail() {
uni.reLaunch({
url: '/pages/index/index'
})
}
})
// #endif
}
</script>
<script lang="ts">
export default {
options: {
styleIsolation: "apply-shared",
virtualHost: true,
addGlobalClass: true,
multipleSlots: true
}
}
</script>
<template>
<view>
<view class="tmNavbar" :style="{ zIndex: zIndex }">
<view :key="_linearActiveGradient" :style="_styleMap" class="tmNavbarBg"></view>
<view class="tmNavbarContentBox">
<view :style="{ height: (statusBarHeight) + 'px' }"></view>
<view class="tmNavbarContent" :style="{ height: (navbarHeight) + 'px' }">
<view class="tmNavbarLeft" :style="{ width: _llWidth }">
<view v-if="showNavBack" @click="backGo" class="tmNavbarBack" style="padding-left: 8px;">
<tm-icon size="52" name="arrow-left-s-line"
:color="isFiexd ? _titleActiveColor : _backColor"></tm-icon>
</view>
<!--
@slot 左边插槽
@binding {boolean} isFiexd - 当前是处于悬浮状态还是静态顶部
-->
<slot name="left" :isFiexd="isFiexd"></slot>
</view>
<view class="tmNavbarTtitle">
<!--
@slot 标题插槽
@binding {boolean} isFiexd - 当前是处于悬浮状态还是静态顶部
-->
<slot name="title">
<text class="xNvbarTtitleOverls"
:style="{ fontSize: _titleFontSize, fontWeight: 'bold', color: isFiexd ? _titleActiveColor : _titleColor }">{{
_title }}</text>
</slot>
</view>
<view class="tmNavbarRight" :style="{ width: _lrWidth }">
<!--
@slot 右边插槽
@binding {boolean} isFiexd - 当前是处于悬浮状态还是静态顶部
-->
<slot name="right" :isFiexd="isFiexd"></slot>
</view>
</view>
</view>
</view>
<view v-if="isPlace" :style="{ height: (statusBarHeight + navbarHeight) + 'px' }"></view>
</view>
</template>
<style scoped>
.xNvbarTtitleOverls {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-clamp: 1;
-webkit-box-orient: vertical;
width: 100%;
text-align: center;
}
.tmNavbarContentBox {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.tmNavbarBack {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.tmNavbar {
position: fixed;
width: 100%;
}
.tmNavbarContent {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.tmNavbarLeft {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.tmNavbarRight {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.tmNavbarTtitle {
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
@@ -0,0 +1,543 @@
<script lang="ts" setup>
import { ref, computed, onMounted, watch, 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-picker-date
* @category 表单组件
* @description 可以设置为年月日,并设置精确到秒。默认的开始时间为当前时间的上一年,结束时间为默认当前时间
* @constant 平台兼容
* | H5 | uniAPP | 小程序 | version |
| --- | --- | --- | --- |
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
*/
defineOptions({ name: 'TmPickerDate' });
const { config } = useTmConfig()
type coverValue = {
value: string[],
str: string
}
type ModelType = "year" | "month" | "day" | "hour" | "minute" | "second";
const props = defineProps({
/**
* 当前时间,与modelStr不同,此提供的值必须是正常的时间格式
* 否则报错,无法运行。
*/
modelValue: {
type: String,
default: ""
},
/**
* 当前时间经过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有效
* 有效格式:
* YYYY年
* MM月
* DD日
* hh小时
* mm分钟
* ss秒
*/
format: {
type: String,
default: "YYYY-MM-DD"
},
/**
* 上方的单位名称
*/
cellUnits: {
type: Array as PropType<string[]>,
default: (): string[] => ['年', '月', '日', '时', '分', '秒']
},
/**
* 是否懒加载内部内容。
* 当前你的列表内容非常多,且影响打开的动画性能时,请务必
* 设置此项为true,以获得流畅视觉效果。如果选择数据较少没有必要打开
*/
lazyContent: {
type: Boolean,
default: false
}
});
const emit = defineEmits([
/**
* 取消时触发
*/
'cancel',
/**
* 确认触发
* @param {string} date 当前选中时间id值
*/
'confirm',
/**
* 滑动变换时触发
* @param {string} date - 当前选中时间
*/
'change',
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
'update:modelShow',
/**
* 经格式化后的值。等同v-model:model-str
*/
'update:modelStr',
'update:modelValue'
]);
const show = ref(false);
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 yanchiDuration = ref(false);
const _lazyContent = computed(() => props.lazyContent);
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 openShow = () => {
show.value = true;
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emit('update:modelShow', true);
}
const onClose = () => {
/**
* 变量控制打开状态
* 等同v-model:model-show
*/
emit('update:modelShow', false);
cancelResetDataCol();
if (_lazyContent.value) {
yanchiDuration.value = false;
}
}
const cancelResetDataCol = () => {
defaultModelvalue(new tmDate(props.modelValue).format('YYYY/MM/DD HH:mm:ss'), false)
}
const onOpen = () => {
yanchiDuration.value = true;
}
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, false)
// onConfirm()
}
const onCancel = () => {
emit('cancel');
cancelResetDataCol();
}
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('confirm', 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);
});
watch(() => props.modelShow, (newValue: boolean) => {
if (newValue == show.value) return;
show.value = newValue;
});
onMounted(() => {
yanchiDuration.value = _lazyContent.value ? false : true;
let nowValue = new tmDate(props.modelValue);
defaultModelvalue(nowValue.format('YYYY/MM/DD HH:mm:ss'), props.modelValue != '');
});
</script>
<template>
<view @click="openShow">
<!--
@slot 插槽,默认触发打开选择器。你的默认布局可以放置在这里。
@binding {boolean} show - 控制打开关闭状态
-->
<slot></slot>
</view>
<tm-drawer @open="onOpen" :widthCoverCenter="true" :disabledScroll="true" :title="title" @close="onClose"
@confirm="onConfirm" @cancel="onCancel" :showFooter="true" v-model:show="show" :show-close="true" size="850">
<view v-if="yanchiDuration&&show" class="tmPickerDateWrap">
<picker-item @change-deep="change" :selected-index="_modelValueIndex" :list="dateList"
:cell-units="cellUnits.slice(0, dateList.length)"></picker-item>
</view>
<tm-icon v-if="!yanchiDuration" size="42" color="error" spin name="loader-line"></tm-icon>
</tm-drawer>
</template>
<style scoped>
.tmPickerDateWrap {
/* display: flex;
flex-direction: row; */
}
</style>
@@ -0,0 +1,326 @@
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, type PropType, getCurrentInstance, type ComponentInstance, onUpdated, nextTick, type 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 pickerItem from './picker-item.vue';
const { config } = useTmConfig()
type TM_PICKER_X_ITEM = Record<string, any>
const boxHeight = ref(0)
const id = ('tmPickerItem-' + getUid())
const nowCurrentIndex = ref([0])
let tid = 0
let tid2 = 1
const emits = defineEmits(['changeDeep', 'countChange'])
const props = defineProps({
list: {
type: Array as PropType<TM_PICKER_X_ITEM[]>,
default: (): TM_PICKER_X_ITEM[] => [] as TM_PICKER_X_ITEM[]
},
wrapWight: {
type: Number,
default: 0
},
parentIndex: {
type: Number,
default: 0
},
selectedIndex: {
type: Array as PropType<number[]>,
default: (): number[] => [] as number[]
},
cellUnits: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
fontSize: {
type: String,
default: "30"
},
/**
* 自定义标识id
*/
rangKey: {
type: String,
default: "id"
},
/**
* 自定义标识文本字段名
*/
rangText: {
type: String,
default: "title"
}
})
const _fontSize = computed((): string => {
let fontSize = covetUniNumber(props.fontSize, config.unit)
if (config.fontSizeScale == 1) return fontSize
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * config.fontSizeScale).toString() + getUnit(props.fontSize)
})
const _parentIndex = computed((): number => props.parentIndex)
const _wrapWight = computed((): number => props.wrapWight)
const _selectedIndex = computed((): number[] => props.selectedIndex)
const _unitsName = computed((): string => {
if (props.cellUnits.length == 0) return ""
if (props.cellUnits.length < _parentIndex.value) return ""
return props.cellUnits[_parentIndex.value]! as string
})
const _cellUnits = computed((): string[] => props.cellUnits as string[])
const _list = computed((): TM_PICKER_X_ITEM[] => props.list.slice(0))
const _nowChildren = computed((): TM_PICKER_X_ITEM[] => {
let index = Math.max(0, Math.min(nowCurrentIndex.value[0], _list.value.length - 1))
if (_list.value.length == 0 && index > _list.value.length - 1) return [] as TM_PICKER_X_ITEM[]
let item = _list.value[index]
return item?.children ?? []
})
const _isDark = computed((): boolean => config.mode == 'dark')
const _maskStyle = computed((): string => {
if (_isDark.value) {
return 'background-image:linear-gradient(180deg,rgba(0, 0, 0, 0),rgba(0, 0, 0, 0)),linear-gradient(0deg, rgba(0, 0, 0, 0),rgba(0, 0, 0, 0))'
}
return 'background-image:linear-gradient(180deg,rgba(255,255,255,0),rgba(255,255,255,0)),linear-gradient(0deg, rgba(255,255,255,0),rgba(255,255,255,0))'
})
const _selectedMaskStyle = computed((): string => {
return _isDark.value ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'
})
const getIndexByid = (): number => {
let index = 0
if (_selectedIndex.value.length == 0) return index;
if (_parentIndex.value > _selectedIndex.value.length) return index
index = _selectedIndex.value[_parentIndex.value]
index = Math.max(0, Math.min(index, _list.value.length - 1))
return index;
}
const getIdByindex = (): string | null => {
let id = null;
if (_list.value.length == 0) return id
if (nowCurrentIndex.value.length == 0) return id;
let index = Math.max(0, Math.min(nowCurrentIndex.value[0], _list.value.length - 1))
return _list.value[index].id
}
const mchange = (indexs: number[]) => {
let index = indexs[0];
let parentIndex = _parentIndex.value;
let ids = _selectedIndex.value.slice(0)
if (parentIndex <= ids.length - 1) {
ids.splice(parentIndex, 1, index)
} else {
for (let i = 0; i < _parentIndex.value; i++) {
ids.push(0)
}
ids.push(index)
}
emits("changeDeep", ids)
}
const change = (s: number[]) => {
emits("changeDeep", s)
}
const pushDeepCount = (n: number) => {
emits("countChange", n)
}
const onChange = (event: any) => {
if (event.detail.value.length == 0 || _list.value.length == 0) return;
let indexs = event.detail.value!;
if (indexs.join('') == nowCurrentIndex.value.join('')) return;
let index = indexs[0]
index = Math.max(0, Math.min(index, _list.value.length - 1))
let item = _list.value[index]
if (item.disabled) {
index = nowCurrentIndex.value[0] > index ? index + 1 : index - 1;
index = Math.max(_list.value.length, index)
index = Math.min(0, index)
indexs = [index]
}
nowCurrentIndex.value = indexs;
mchange(indexs)
}
onMounted(() => {
pushDeepCount(_parentIndex.value)
nowCurrentIndex.value = [getIndexByid()] as number[]
})
onBeforeUnmount(() => {
clearTimeout(tid)
clearTimeout(tid2)
})
watch([() => props.selectedIndex], () => {
nowCurrentIndex.value = [getIndexByid()] as number[]
}, { deep: true })
watch(() => props.list, () => {
nowCurrentIndex.value = [getIndexByid()] as number[]
mchange(nowCurrentIndex.value.slice(0))
}, { deep: true })
</script>
<script lang="ts">
export default {
options: {
styleIsolation: "apply-shared",
virtualHost: true,
addGlobalClass: true,
multipleSlots: true,
},
};
</script>
<template>
<view>
<view class="tmPickerViewUnit" v-if="_unitsName != ''">
<tm-text font-size="24" class="tmPickerViewUnitText">{{ _unitsName }}</tm-text>
</view>
<view class="tmPickerView">
<view style="padding: 0 2.5px;">
<picker-view :immediate-change="false" :value="nowCurrentIndex" @change="onChange"
:style="{ height: '250px', width: (_wrapWight - 5) + 'px' }" :mask-style="`${_maskStyle}`"
:indicator-style="`height:50px;border:none;border-radius:10px`">
<picker-view-column>
<view :style="{ background: nowCurrentIndex[0] == index ? _selectedMaskStyle : 'none' }"
v-for="(item, index) in _list" :key="index" class="tmPickerViewWrapCoumn">
<text class="tmPickerViewWrapCoumnText" :style="{
fontSize: _fontSize,
lineHeight: 1,
fontWeight: nowCurrentIndex[0] == index ? 'bold' : 'inherit',
opacity: item.disabled ? 0.4 : (nowCurrentIndex[0] == index ? 1 : 0.6),
color: _isDark ? 'rgba(255,255,255,0.8)' : '#000000'
}">
{{ item[rangText] }}
</text>
</view>
</picker-view-column>
</picker-view>
</view>
<picker-item :rangKey="rangKey" :rangText="rangText" @countChange="pushDeepCount" :font-size="fontSize"
:cellUnits="_cellUnits" @changeDeep="change" :selectedIndex="_selectedIndex" :wrapWight="_wrapWight"
:parentIndex="_parentIndex + 1" v-if="_nowChildren.length > 0" :list="_nowChildren">
</picker-item>
</view>
</view>
</template>
<style>
.uni-picker-view-indicator:after,
.uni-picker-view-indicator:before {
border: none;
}
.tmPickerViewUnit {
display: flex;
flex-direction: row;
justify-content: center;
padding: 8px;
}
.tmPickerViewUnitText {
font-size: 12px;
color: #888;
font-weight: bold;
}
.tmPickerViewWrapCoumnText {
margin: 0 6px;
width: 100%;
text-align: center;
display: -webkit-box;
line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.tmPickerView {
display: flex;
flex-direction: row;
}
.tmPickerViewWrap {
position: relative;
}
.tmPickerContent {
position: absolute;
left: 0;
top: 0;
width: 100%;
z-index: 5;
}
.tmPickerMasker {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
pointer-events: none;
height: 100%;
width: 100%;
top: 0px;
left: 0px;
position: absolute;
}
.xPickErBar {
background-color: #f5f5f5;
border-radius: 10px;
margin: 0 3px;
flex: 1;
}
.tmPickerContent {
transition-duration: 350ms;
transition-property: left, right, top, bottom;
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
}
.tmPickerViewWrapCoumn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 50px;
border-radius: 10px;
/* background-color: #f5f5f5; */
}
</style>
@@ -0,0 +1,242 @@
<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 pickerItem from './picker-item.vue';
const { config } = useTmConfig()
type TM_PICKER_X_ITEM = Record<string, any>
const boxHeight = ref(0)
const id = ('tmPickerItem-' + getUid())
const nowCurrentIndex = ref([0])
let tid = 0
let tid2 = 1
const emits = defineEmits(['changeDeep'])
const props = defineProps({
list: {
type: Array as PropType<TM_PICKER_X_ITEM[][]>,
default: (): TM_PICKER_X_ITEM[] => [] as TM_PICKER_X_ITEM[]
},
selectedIndex: {
type: Array as PropType<number[]>,
default: (): number[] => [] as number[]
},
cellUnits: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
fontSize: {
type: String,
default: "30"
},
/**
* 自定义标识id
*/
rangKey: {
type: String,
default: "id"
},
/**
* 自定义标识文本字段名
*/
rangText: {
type: String,
default: "title"
},
})
const _fontSize = computed((): string => {
let fontSize = covetUniNumber(props.fontSize, config.unit)
if (config.fontSizeScale == 1) return fontSize
let sizeNumber = parseInt(fontSize)
if (isNaN(sizeNumber)) {
sizeNumber = 16
}
return (sizeNumber * config.fontSizeScale).toString() + getUnit(props.fontSize)
})
const refreshKey = ref(12)
const _cellUnits = computed((): string[] => props.cellUnits as string[])
const _list = computed((): TM_PICKER_X_ITEM[][] => props.list.slice(0))
const _isDark = computed((): boolean => config.mode == 'dark')
const _maskStyle = computed((): string => {
if (_isDark.value) {
return 'background-image:linear-gradient(180deg,rgba(0, 0, 0, 0),rgba(0, 0, 0, 0)),linear-gradient(0deg, rgba(0, 0, 0, 0),rgba(0, 0, 0, 0))'
}
return 'background-image:linear-gradient(180deg,rgba(255,255,255,0),rgba(255,255,255,0)),linear-gradient(0deg, rgba(255,255,255,0),rgba(255,255,255,0))'
})
const _selectedMaskStyle = computed((): string => {
return _isDark.value ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'
})
const onChange = (event: any) => {
if (event.detail.value.length == 0 || _list.value.length == 0) return;
let indexs = event.detail.value!;
if (indexs.join('') == nowCurrentIndex.value.join('')) return;
nowCurrentIndex.value = indexs
emits("changeDeep", indexs)
}
onMounted(() => {
nowCurrentIndex.value = props.selectedIndex.slice(0)
})
onBeforeUnmount(() => {
clearTimeout(tid)
clearTimeout(tid2)
})
watch(() => props.selectedIndex, (newvalue: number[]) => {
if (newvalue.join('') == nowCurrentIndex.value.join('')) return;
nowCurrentIndex.value = newvalue.slice(0)
}, { deep: true })
watch(_list, (newvalue) => {
refreshKey.value += 1;
}, { deep: true })
</script>
<script lang="ts">
export default {
options: {
styleIsolation: "apply-shared",
virtualHost: true,
addGlobalClass: true,
multipleSlots: true,
},
};
</script>
<template>
<view>
<view class="tmPickerViewUnit" v-if="_cellUnits.length > 0">
<view v-for="(item, index) in _cellUnits" :key="index" style="flex: 1;text-align: center;">
<tm-text font-size="24" class="tmPickerViewUnitText">{{ item }}</tm-text>
</view>
</view>
<picker-view :key="refreshKey" :immediate-change="true" :value="nowCurrentIndex" @change="onChange"
:style="{ height: '250px', width: '100%' }" :mask-style="`${_maskStyle}`"
:indicator-style="`height:50px;border:none;border-radius:10px`">
<picker-view-column v-for="(item2, index2) in _list" :key="index2">
<view
:style="{ background: nowCurrentIndex[index2] == index ? _selectedMaskStyle : 'none', margin: '0 4px' }"
v-for="(item, index) in item2" :key="index" class="tmPickerViewWrapCoumn">
<text class="tmPickerViewWrapCoumnText" :style="{
fontSize: _fontSize,
lineHeight: 1,
fontWeight: nowCurrentIndex[index2] == index ? 'bold' : 'inherit',
opacity: item.disabled ? 0.4 : (nowCurrentIndex[index2] == index ? 1 : 0.6),
color: _isDark ? 'rgba(255,255,255,0.8)' : '#000000'
}">
{{ item[rangText] || "" }}
</text>
</view>
</picker-view-column>
</picker-view>
</view>
</template>
<style>
.uni-picker-view-indicator:after,
.uni-picker-view-indicator:before {
border: none;
}
.tmPickerViewUnit {
display: flex;
flex-direction: row;
justify-content: center;
padding: 8px;
}
.tmPickerViewUnitText {
font-size: 12px;
color: #888;
font-weight: bold;
}
.tmPickerViewWrapCoumnText {
margin: 0 6px;
width: 100%;
text-align: center;
display: -webkit-box;
line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.tmPickerView {
display: flex;
flex-direction: row;
}
.tmPickerViewWrap {
position: relative;
}
.tmPickerContent {
position: absolute;
left: 0;
top: 0;
width: 100%;
z-index: 5;
}
.tmPickerMasker {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
pointer-events: none;
height: 100%;
width: 100%;
top: 0px;
left: 0px;
position: absolute;
}
.xPickErBar {
background-color: #f5f5f5;
border-radius: 10px;
margin: 0 3px;
flex: 1;
}
.tmPickerContent {
transition-duration: 350ms;
transition-property: left, right, top, bottom;
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
}
.tmPickerViewWrapCoumn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 50px;
border-radius: 10px;
/* background-color: #f5f5f5; */
}
</style>
@@ -0,0 +1,283 @@
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, 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 pickerItem from './picker-item.vue';
type TM_PICKER_X_ITEM = Record<string, any>
type TM_PICKER_ITEM_INFO = Record<string, any>
const proxy = getCurrentInstance()?.proxy ?? null;
/**
* @displayName 选择器容器
* @exportName tm-picker-view
* @category 表单组件
* @description 可单列或者多列
* @constant 平台兼容
* | H5 | uniAPP | 小程序 | version |
| --- | --- | --- | --- |
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
*/
defineOptions({ name: 'TmPickerView' });
const { config } = useTmConfig()
const wrapTotalWidth = ref(0)
const nowValue = ref<Array<string | number>>([])
const _modelValueIndex = ref<Array<number>>([])
const okNodeInfo = ref(false)
const tid = ref(0)
const parentIndex = ref(0)
const emits = defineEmits([
/**
* 选项变化时触发
* @param {string[]} ids - 当前选中项的id
*/
'change',
/**
* 等同v-model:model-str
* 只对外输出当前回选区的选中项的文本,不要外部改变此值。
*/
'update:modelStr',
'update:modelValue'
])
const props = defineProps({
/**
* 数据项
* 格式类型为:PICKER_ITEM_INFO
*/
list: {
type: Array as PropType<TM_PICKER_ITEM_INFO[]>,
default: (): TM_PICKER_ITEM_INFO[] => [] as TM_PICKER_ITEM_INFO[]
},
/**
* 当前选中项的id值
*/
modelValue: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
/**
* 当前选中项的标题文本组
*/
modelStr: {
type: String,
default: ""
},
/**
* 显示在顶部的单位名称
*/
cellUnits: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
/**
* 项目的字体号大小
*/
fontSize: {
type: String,
default: "32"
},
/**
* 自定义标识id
*/
rangKey: {
type: String,
default: "id"
},
/**
* 自定义标识文本字段名
*/
rangText: {
type: String,
default: "title"
}
})
const _cellUnits = computed(() => props.cellUnits)
const _list = computed(() => props.list)
const _maxDeep = computed(() => {
if (_list.value.length == 0) return 0;
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
let deepIndex = 1;
const node = list[0];
if (node?.children?.length > 0) {
deepIndex += getdiepWidth(node.children);
}
return deepIndex;
}
return getdiepWidth(_list.value.slice(0));
})
const _deepWidth = computed(() => {
if (_list.value.length == 0) return 0;
if (_list.value.length == 0) return wrapTotalWidth.value;
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
let deepIndex = 1;
const node = list[0];
if (Array.isArray(node?.children) && node.children.length > 0) {
deepIndex += getdiepWidth(node.children);
}
return deepIndex;
}
return wrapTotalWidth.value / Math.max(1, getdiepWidth(_list.value))
})
const setChangeStrvmodel = () => {
let listitem = getModelStr()
let idvalue = [] as string[]
let strs = [] as string[]
listitem.forEach((el) => {
idvalue.push(el[props.rangKey])
strs.push(el[props.rangText])
})
emits('update:modelStr', strs.join(","))
}
const oninitFun = () => {
nowValue.value = props.modelValue.slice(0)
_modelValueIndex.value = chuliIndex(getIdeByindex());
setChangeStrvmodel()
getNodeInfo();
}
const change = (ids: number[]) => {
_modelValueIndex.value = ids;
let listitem = getModelStr()
let idvalue = [] as string[]
let strs = [] as string[]
listitem.forEach((el) => {
idvalue.push(el[props.rangKey])
strs.push(el[props.rangText])
})
/**
* 等同v-model
*/
emits('update:modelValue', idvalue)
emits('update:modelStr', strs.join(","))
if (idvalue.join('') == props.modelValue.join('')) return;
clearTimeout(tid.value);
tid.value = setTimeout(function () {
/**
* 选项变化时触发
* @param {string[]} ids 当前选中项的id
*/
emits('change', idvalue)
}, 30);
}
const chuliIndex = (indexs: number[]): number[] => {
let ids = indexs.slice(0)
let val = nowValue.value.slice(0)
for (let i = 0; i < _maxDeep.value; i++) {
if (i >= val.length) {
ids.push(0)
}
}
return ids;
}
const getIdeByindex = (): number[] => {
let index = 0;
let val = nowValue.value.slice(0)
let indexs = [] as number[]
function getIndex(nodes: TM_PICKER_X_ITEM[]) {
if (val.length <= index || val.length == 0) return;
let id = val[index]
let sindex = 0
for (let i = 0; i < nodes.length; i++) {
let item = nodes[i]
if (item[props.rangKey] == id) {
sindex = i;
indexs.push(sindex)
if (item?.children?.length > 0) {
index += 1
getIndex(item.children)
}
}
}
}
getIndex(_list.value)
return indexs;
}
const getModelStr = (): TM_PICKER_X_ITEM[] => {
let ids = _modelValueIndex.value.slice(0)
let index = 0;
let selectedList = [] as TM_PICKER_X_ITEM[]
function getStr(nodes: TM_PICKER_X_ITEM[]) {
if (ids.length <= index || ids.length == 0) return;
let idx = ids[index]
idx = Math.max(0, Math.min(nodes.length - 1, idx));
let item = nodes[idx];
selectedList.push(item)
if (item?.children?.length > 0) {
index += 1
getStr(item.children)
}
}
getStr(_list.value)
return selectedList
}
const conutChangeEnd = (n: number) => {
}
const getNodeInfo = () => {
uni.createSelectorQuery().in(proxy)
.select(".tmPickerView")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as UniApp.NodeInfo
wrapTotalWidth.value = nodeinfo.width!
okNodeInfo.value = true;
})
}
onMounted(() => {
oninitFun();
uni.$on('onResize', oninitFun)
})
onBeforeUnmount(() => {
clearTimeout(tid.value);
uni.$off('onResize', oninitFun)
})
watch(() => props.modelValue, (newValue: Array<string | number>) => {
if (newValue.join('') == nowValue.value.join('')) return;
nowValue.value = newValue.slice(0);
_modelValueIndex.value = chuliIndex(getIdeByindex());
setChangeStrvmodel()
}, { deep: true })
</script>
<script lang="ts">
export default {
options: {
styleIsolation: "apply-shared",
virtualHost: true,
addGlobalClass: true,
multipleSlots: true,
},
};
</script>
<template>
<view class="tmPickerView">
<picker-item :rangKey="props.rangKey" :rangText="props.rangText" @countChange="conutChangeEnd"
:font-size="props.fontSize" :cellUnits="_cellUnits" v-if="okNodeInfo" @changeDeep="change"
:selectedIndex="_modelValueIndex" :parentIndex="parentIndex" :wrapWight="_deepWidth" :list="_list"
class="tmPickerViewItem"></picker-item>
</view>
</template>
<style scoped>
.tmPickerView {
display: flex;
}
</style>
@@ -0,0 +1,276 @@
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, PropType, getCurrentInstance, 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 pickerItem from './picker-item.vue';
type TM_PICKER_X_ITEM = Record<string, any>
type TM_PICKER_ITEM_INFO = Record<string, any>
const proxy = getCurrentInstance()?.proxy ?? null;
/**
* @displayName 选择器容器
* @exportName tm-picker-view
* @category 表单组件
* @description 可单列或者多列
* @constant 平台兼容
* | H5 | uniAPP | 小程序 | version |
| --- | --- | --- | --- |
| ☑️| ☑️ | ☑️ | ☑️ | ☑️ | 1.0.0 |
*/
defineOptions({ name: 'TmPickerView' });
const { config } = useTmConfig()
const nowValue = ref<Array<string | number>>([])
const _modelValueIndex = ref<Array<number>>([])
const tid = ref(0)
const emits = defineEmits([
/**
* 选项变化时触发
* @param {string[]} ids - 当前选中项的id
*/
'change',
/**
* 等同v-model:model-str
* 只对外输出当前回选区的选中项的文本,不要外部改变此值。
*/
'update:modelStr',
'update:modelValue'
])
const props = defineProps({
/**
* 数据项
* 格式类型为:PICKER_ITEM_INFO
*/
list: {
type: Array as PropType<TM_PICKER_ITEM_INFO[]>,
default: (): TM_PICKER_ITEM_INFO[] => [] as TM_PICKER_ITEM_INFO[]
},
/**
* 当前选中项的id值
*/
modelValue: {
type: Array<string|number>,
default: (): string[] => [] as string[]
},
/**
* 当前选中项的标题文本组
*/
modelStr: {
type: String,
default: ""
},
/**
* 显示在顶部的单位名称
*/
cellUnits: {
type: Array as PropType<string[]>,
default: (): string[] => [] as string[]
},
/**
* 项目的字体号大小
*/
fontSize: {
type: String,
default: "32"
},
/**
* 自定义标识id
*/
rangKey: {
type: String,
default: "id"
},
/**
* 自定义标识文本字段名
*/
rangText: {
type: String,
default: "title"
}
})
const _cellUnits = computed(() => props.cellUnits)
const _list = computed(() => props.list)
const _nowShowList = ref<Record<string,any>[][]>([])
const _maxDeep = computed(() => {
if (_list.value.length == 0) return 0;
function getdiepWidth(list: TM_PICKER_X_ITEM[]): number {
let deepIndex = 1;
const node = list[0];
if (node?.children?.length > 0) {
deepIndex += getdiepWidth(node.children);
}
return deepIndex;
}
return getdiepWidth(_list.value.slice(0));
})
const oninitFun = () => {
nowValue.value = props.modelValue.slice(0)
let currneinexs = getIndexsByids(nowValue.value).indexs;
_modelValueIndex.value = nowValue.value.length==0||currneinexs.length==0?new Array(_maxDeep.value).fill(0):currneinexs;
const {data,strs,ids} = getIdsByindexs(_modelValueIndex.value)
_nowShowList.value = data.slice(0)
if(nowValue.value.length==0){
nowValue.value = ids.slice(0)
nextTick(()=>{
emits('update:modelStr', strs.join(","))
emits('update:modelValue', ids.slice(0))
})
}
}
const change = (ixs: number[]) => {
const {ids,data,strs} = getIdsByindexs(ixs)
_modelValueIndex.value = getIndexsByids(ids).indexs;
_nowShowList.value = data.slice(0)
nowValue.value = ids;
/**
* 等同v-model
*/
emits('update:modelValue', ids.slice(0))
emits('update:modelStr', strs.join(","))
if (strs.join('') == props.modelValue.join('')) return;
emits('change', ids.slice(0))
}
// 根据id返回索引
const getIndexsByids = (ids:Array<string|number>):{indexs:Array<number>,data:TM_PICKER_X_ITEM[][],strs:string[]}=> {
let index = 0;
let val = ids.slice(0)
let indexs = [] as number[]
let data = [] as TM_PICKER_X_ITEM[][]
let strs = [] as string[]
if(_list.value.length==0||ids.length==0) return {indexs:[],data:[],strs:[]}
function getIndex(nodes: TM_PICKER_X_ITEM[]) {
if (val.length <= index || val.length == 0) return;
let id = val[index]
let sindex = 0
for (let i = 0; i < nodes.length; i++) {
let item = nodes[i]
if (item[props.rangKey] == id) {
sindex = i;
indexs.push(sindex)
data.push(nodes)
strs.push(item[props.rangText])
if (item?.children?.length > 0) {
index += 1
getIndex(item.children)
}
}
}
}
getIndex(_list.value)
return {indexs,data,strs}
}
//根据索引返回ids
const getIdsByindexs = (indexs:number[]):{ids:Array<string|number>,data:TM_PICKER_X_ITEM[][],strs:string[]} => {
let ids = [] as string[]
let data = [] as TM_PICKER_X_ITEM[][]
let index = 0;
let val = indexs.slice(0)
let strs = [] as string[]
if(_list.value.length==0||indexs.length==0) return {ids:[],data:[],strs:[]}
function getIds(nodes: TM_PICKER_X_ITEM[]) {
if (val.length <= index || val.length == 0) return;
let currentsIndex = val[index]
let id = ''
let str = ''
let children = [];
if(nodes.length-1 >= currentsIndex){
id = nodes[currentsIndex][props.rangKey]
str = nodes[currentsIndex][props.rangText]
children = nodes[currentsIndex]?.children??[]
}else{
id = nodes[0][props.rangKey]
str = nodes[0][props.rangText]
children = nodes[0]?.children??[]
}
ids.push(id)
data.push(nodes)
strs.push(str)
if(children.length>0){
index += 1
getIds(children)
}
}
getIds(_list.value)
return {ids,data,strs};
}
const getNowCurrent = ()=>{
return getIdsByindexs(_modelValueIndex.value)
}
// 获取相关数据。
onMounted(() => {
oninitFun();
uni.$on('onResize', oninitFun)
})
onBeforeUnmount(() => {
clearTimeout(tid.value);
uni.$off('onResize', oninitFun)
})
watch(() => props.modelValue, (newValue: Array<string | number>) => {
if (newValue.join('') == nowValue.value.join('')) return;
nowValue.value = newValue.slice(0);
let defauleindexs = nowValue.value.length==0?new Array(_maxDeep.value).fill(0):getIndexsByids(nowValue.value).indexs;
_modelValueIndex.value = defauleindexs;
const {data,strs} = getIdsByindexs(_modelValueIndex.value)
_nowShowList.value = data.slice(0)
emits('update:modelStr', strs.join(","))
}, { deep: true })
watch(() => props.list, (newValue) => {
oninitFun()
}, { deep: true })
defineExpose({getIndexsByids,getIdsByindexs,getNowCurrent,onInit:oninitFun})
</script>
<script lang="ts">
export default {
options: {
styleIsolation: "apply-shared",
virtualHost: true,
addGlobalClass: true,
multipleSlots: true,
},
};
</script>
<template>
<view class="tmPickerView">
<picker-item :rangKey="props.rangKey" :rangText="props.rangText"
:font-size="props.fontSize" :cellUnits="_cellUnits" @changeDeep="change"
:selectedIndex="_modelValueIndex" :list="_nowShowList"
class="tmPickerViewItem"></picker-item>
</view>
</template>
<style scoped>
.tmPickerView {
}
</style>
@@ -0,0 +1,129 @@
type TM_PICKER_X_ITEM = Record<string, any>
/**
* 当前树有多少个被选中了。
*/
export function getTreeSelectedNum(item: TM_PICKER_X_ITEM[], target: Set<string>): number {
let inx = 0;
function jshz(tree: TM_PICKER_X_ITEM[]) {
for (let i = 0; i < tree.length; i++) {
let item = tree[i]
if (item.children.length > 0) {
jshz(item.children)
} else if (target.has(item.id)) {
inx += 1
}
}
}
jshz(item)
return inx;
}
/**
* 根据目标节点,过滤掉父节点。
* @param item
* @param target
*/
export function filterParentNode(item: TM_PICKER_X_ITEM[], target: Set<string>): string[] {
function jshz(tree: TM_PICKER_X_ITEM[]): string[] {
let arr = [] as string[]
for (let i = 0; i < tree.length; i++) {
let item = tree[i]
if (item.children.length > 0) {
arr = arr.concat(jshz(item.children))
} else if (target.has(item.id)) {
arr.push(item.id)
}
}
return arr;
}
return jshz(item)
}
/**
* 根据给定的长度值,赋值默认值
*/
export function setDefaultByValueAr(list: TM_PICKER_X_ITEM[], value: string[], maxLen: number): string[] {
let v = value;
let pr = [] as string[];
for (let j = 0; j < maxLen; j++) {
pr.push("")
}
v = v.concat(pr).slice(0, maxLen)
let defaultAr = [] as string[];
function setvalue(item: TM_PICKER_X_ITEM[]) {
if (item.length > 0) {
defaultAr.push(item[0].id);
if (item[0].children.length > 0) {
setvalue(item[0].children)
}
}
}
setvalue(list);
for (let i = 0; i < maxLen; i++) {
if (defaultAr.length - 1 >= i && v[i] == "") {
v[i] = defaultAr[i]
}
}
return v;
}
// 通过 ID 查找节点的函数
export function findNodeById(TreeNode: TM_PICKER_X_ITEM[], id: string): TM_PICKER_X_ITEM | null {
let newnode = null as TM_PICKER_X_ITEM | null
for (let i = 0; i < TreeNode.length; i++) {
let node = TreeNode[i]
if (node.id === id) {
newnode = node;
break;
}
if (node.children.length > 0) {
const foundNode = findNodeById(node.children, id);
if (foundNode != null) {
newnode = foundNode;
break;
}
}
}
return newnode;
}
// 查找节点的上级节点
export function findParentNode(nodes: TM_PICKER_X_ITEM[], targetNodeId: string): TM_PICKER_X_ITEM | null {
function search(node: TM_PICKER_X_ITEM): TM_PICKER_X_ITEM | null {
if (node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child.id === targetNodeId) {
return node;
} else {
const foundParent = search(child);
if (foundParent != null) {
return foundParent;
}
}
}
}
return null;
}
// 使用for循环遍历根节点数组
for (let i = 0; i < nodes.length; i++) {
const root = nodes[i];
const parentNode = search(root);
if (parentNode != null) {
return parentNode;
}
}
return null; // 如果未找到目标节点的父节点,则返回undefined
}
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more