feat: 把hello项目文件搬过来

This commit is contained in:
菲鸽 2024-02-04 16:00:27 +08:00
parent 16694caccd
commit b90a409737
45 changed files with 2398 additions and 11 deletions

View File

@ -15,4 +15,34 @@ export default defineUniPages({
'^uv-(.*)': '@climblee/uv-ui/components/uv-$1/uv-$1.vue',
},
},
tabBar: {
color: '#999999',
selectedColor: '#018d71',
backgroundColor: '#F8F8F8',
borderStyle: 'black',
height: '50px',
fontSize: '10px',
iconWidth: '24px',
spacing: '3px',
list: [
{
iconPath: 'static/tabbar/home.png',
selectedIconPath: 'static/tabbar/homeHL.png',
pagePath: 'pages/index/index',
text: '首页',
},
{
iconPath: 'static/tabbar/example.png',
selectedIconPath: 'static/tabbar/exampleHL.png',
pagePath: 'pages/demo/index',
text: '示例',
},
{
iconPath: 'static/tabbar/personal.png',
selectedIconPath: 'static/tabbar/personalHL.png',
pagePath: 'pages/my/index',
text: '我的',
},
],
},
})

View File

@ -0,0 +1,3 @@
<template>
<view class="text-green-500"> header </view>
</template>

View File

@ -0,0 +1,7 @@
# fly-login
点击“点击显示微信头像”按钮后,出现的半屏登录弹窗,可以在任意页面引入。
仿“掘金小册”小程序。
![掘金小册登录](screenshot.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,120 @@
<template>
<view class="fly-login" v-if="modelValue">
<view class="fly-login-mask" />
<view class="fly-login-content px-4">
<view class="font-bold h-16 leading-16">获取您的昵称头像</view>
<view
class="rounded-full bg-light-600 w-6 h-6 text-center absolute top-4 right-4"
@click="onClose"
>
<view class="i-carbon-close text-gray-700" />
</view>
<view
class="flex items-center h-16 leading-16 border-b-gray-400 border-b-solid border-[1rpx]"
>
<text class="mr-4 flex-shrink-0">头像</text>
<button
class="bg-transparent flex items-center after:b-none w-full h-12 leading-12"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
<image class="w-8 h-8 rounded-full" :src="avatarUrl"></image>
<text class="ml-auto i-carbon-chevron-right"></text>
</button>
</view>
<view
class="flex items-center h-16 leading-16 border-b-gray-400 border-b-solid border-1 mt-4"
>
<text class="mr-4 flex-shrink-0">昵称</text>
<input type="nickname" placeholder="请输入昵称" @change="onChange" @blur="onChange" />
</view>
<button
size="default"
type="default"
style="color: #fff; background-color: #1aad19; border-color: #1aad19"
class="text-center leading-12 w-40 my-4"
@click="onSubmit"
>
确定
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store'
import defaultAvatarUrl from './defaultAvatar.png'
const emit = defineEmits(['update:modelValue'])
defineProps<{ modelValue: boolean }>()
const userStore = useUserStore()
const avatarUrl = ref(defaultAvatarUrl)
const nickname = ref('')
const onClose = () => {
emit('update:modelValue', false)
}
const onChooseAvatar = (e) => {
const { avatarUrl: url } = e.detail
avatarUrl.value = url
//
console.log(url)
}
const onChange = (e) => {
const { value } = e.detail
nickname.value = value
console.log(value)
}
const onSubmit = () => {
// 1
// 2
if (avatarUrl.value === defaultAvatarUrl) {
uni.showToast({
title: '请选择头像',
icon: 'none',
})
return
}
if (!nickname.value) {
uni.showToast({
title: '请填写昵称',
icon: 'none',
})
return
}
emit('update:modelValue', false)
console.log('保存用户信息')
userStore.setUserInfo({ nickname: nickname.value, avatar: avatarUrl.value })
}
</script>
<style lang="scss" scoped>
.fly-login {
position: fixed;
inset: 0;
.fly-login-mask {
position: fixed;
inset: 0;
background-color: rgb(0 0 0 / 30%);
}
.fly-login-content {
position: fixed;
right: 0;
bottom: var(--window-bottom);
left: 0;
background-color: #fff;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

View File

@ -0,0 +1,3 @@
# fly-navbar
建议本导航栏组件在设置 `"navigationStyle": "custom"` 的页面使用,目前支持微信小程序的页面滚动动画。

View File

@ -0,0 +1,71 @@
<template>
<!-- 自定义导航栏: 默认透明不可见, scroll-view 滚动到 50 时展示 -->
<view class="fly-navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<!-- 1/3多于1个页面用返回图标 -->
<navigator v-if="pages.length > 1" open-type="navigateBack" class="left-icon">
<view class="bg-gray-500/80 rounded-full w-8 h-8 flex items-center justify-center">
<button class="i-carbon-chevron-left text-white w-7 h-7"></button>
</view>
</navigator>
<!-- 2/3只有1个页面如果不是tabbar需要首页图标 -->
<!-- 这种情况一般出现在用户直接打开分享出去的详情页面或者使用redirectTo等API -->
<navigator
v-else-if="!isTabbar"
open-type="switchTab"
url="/pages/index/index"
class="left-icon"
>
<view class="bg-gray-500/80 rounded-full w-8 h-8 flex items-center justify-center">
<button class="i-carbon-home text-white w-6 h-6"></button>
</view>
</navigator>
<!-- 3/3如果当前页就是tabbar页不用去首页也就是什么图标都不需要 -->
<view class="title">{{ title || '' }}</view>
</view>
</template>
<script lang="ts" setup>
import { getIsTabbar } from '@/utils/index'
defineProps<{ title?: string }>()
//
const pages = getCurrentPages()
const isTabbar = getIsTabbar()
console.log({ isTabbar, pagesLen: pages.length })
//
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>
<style lang="scss" scoped>
.fly-navbar {
position: fixed;
top: 0;
left: 0;
z-index: 9;
width: 750rpx;
color: #000;
background-color: transparent;
.left-icon {
position: absolute;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
font-size: 44rpx;
color: #000;
}
.title {
display: flex;
align-items: center;
justify-content: center;
height: 44px;
font-size: 32rpx;
color: transparent;
}
}
</style>

View File

@ -0,0 +1,62 @@
import { onReady } from '@dcloudio/uni-app'
import { getIsTabbar } from '@/utils/index'
export default () => {
// 获取页面栈
const pages = getCurrentPages()
const isTabbar = getIsTabbar()
// 页面滚动到底部时的操作,通常用于加载更多数据
const onScrollToLower = () => {}
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
// #ifdef MP-WEIXIN
// 基于小程序的 Page 类型扩展 uni-app 的 Page
type PageInstance = Page.PageInstance & WechatMiniprogram.Page.InstanceMethods<any>
// 获取当前页面实例,数组最后一项
const pageInstance = getCurrentPages().at(-1) as PageInstance
// 页面渲染完毕,绑定动画效果
onReady(() => {
// 动画效果,导航栏背景色
pageInstance.animate(
'.fly-navbar',
[{ backgroundColor: 'transparent' }, { backgroundColor: '#f8f8f8' }],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// 动画效果,导航栏标题
pageInstance.animate(
'.fly-navbar .title',
[{ color: 'transparent' }, { color: '#000' }],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// 动画效果,导航栏返回按钮
pageInstance.animate('.fly-navbar .left-icon', [{ color: '#fff' }, { color: '#000' }], 1000, {
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
})
})
// #endif
return {
pages,
isTabbar,
onScrollToLower,
safeAreaInsets,
}
}

View File

@ -0,0 +1,25 @@
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
export default () => {
return {
/** 激活“分享给好友” */
onShareAppMessage: onShareAppMessage(
(options: Page.ShareAppMessageOption): Page.CustomShareContent => {
console.log('options:', options)
return {
title: '自定义分享标题',
path: '/pages/index/index?id=xxx',
imageUrl:
'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/pretty-girl.png',
}
},
),
/** 激活“分享到朋友圈”, 注意:需要先激活“分享给好友” */
onShareTimeline: onShareTimeline((): Page.ShareTimelineContent => {
return {
title: '自定义分享标题',
query: 'a=1&b=2',
}
}),
}
}

View File

@ -0,0 +1,42 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '微信分享' },
}
</route>
<template>
<view class="text-green">微信分享页</view>
<view class="text-green-500">请在微信小程序中体验或者开发者工具</view>
<view>1) 默认是不激活发送给朋友分享到朋友圈如下图</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/wx-share/wx-share-before.png"
mode="widthFix"
/>
<view>2) 增加了onShareAppMessage和onShareTimeline后就可以微信分享了如下图</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/wx-share/wx-share-after.png"
mode="widthFix"
/>
</template>
<script lang="ts" setup>
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
/** 激活“分享给好友” */
onShareAppMessage((options: Page.ShareAppMessageOption): Page.CustomShareContent => {
console.log('options:', options)
return {
title: '自定义分享标题',
path: '/pages/index/index?id=xxx',
imageUrl:
'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/pretty-girl.png',
}
})
/** 激活“分享到朋友圈”, 注意:需要先激活“分享给好友” */
onShareTimeline((): Page.ShareTimelineContent => {
return {
title: '自定义分享标题',
query: 'a=1&b=2',
}
})
</script>

View File

@ -0,0 +1,19 @@
<route lang="json5">
{
style: { navigationBarTitleText: '自定义导航栏', navigationStyle: 'custom' },
}
</route>
<template>
<fly-navbar />
<view class="bg-green-300 min-h-20" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<view class="p-4"> 自定义导航栏设置"navigationStyle":"custom" </view>
<view class="p-4"> 通常页面顶部有一个图片或背景色 </view>
</view>
<fly-content :line="20" />
</template>
<script lang="ts" setup>
//
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>

View File

@ -0,0 +1,40 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: 'pinia+持久化' },
}
</route>
<template>
<view class="flex justify-center items-center text-blue-500 mt-4 mb-4">
<view class="w-20">Count: {{ countStore.count }}</view>
<button class="ml-2 mr-2" @click="countStore.decrement">-1</button>
<button class="ml-2 mr-2" @click="countStore.increment">+1</button>
<button class="ml-2 mr-2" @click="countStore.reset">重置</button>
</view>
<view class="m-8 text-4 leading-8">
<view class="text-center">{{ userStore.userInfo }}</view>
<view class="text-center">请观察小程序的store可以看到是可以正常设置的</view>
<button @click="setUserInfo">设置UserInfo</button>
<button @click="clearUserInfo" class="mt-4">清除UserInfo</button>
<button @click="resetUserStore" class="mt-4">reset UserStore</button>
</view>
</template>
<script lang="ts" setup>
import { useCountStore, useUserStore } from '@/store'
const countStore = useCountStore()
const userStore = useUserStore()
const setUserInfo = () => {
userStore.setUserInfo({ nickname: 'fly', avatar: '', token: 'abcdef' })
}
const clearUserInfo = () => {
userStore.clearUserInfo()
}
const resetUserStore = () => {
userStore.reset()
}
</script>

View File

@ -0,0 +1,67 @@
<route lang="json5">
{
layout: 'demo',
style: {
navigationBarTitleText: '请求',
},
}
</route>
<template>
<view class="mt-6">
<!-- http://localhost:9100/#/pages/index/request -->
<button @click="getFoo" class="my-4">测试 GET 请求</button>
<view class="text-xl">请求数据如下</view>
<view class="text-green h-10">{{ JSON.stringify(data) }}</view>
<view class="text-xl">完整数据</view>
<view class="text-green h-20">{{ JSON.stringify(originalData) }}</view>
<button @click="postFoo" class="my-4">测试 POST 请求</button>
<view class="text-xl">请求数据如下</view>
<view class="text-green h-10">{{ JSON.stringify(data2) }}</view>
<button class="my-8" type="warn" @click="reset">一键清空数据</button>
<view class="my-2">使用的是 laf 云后台</view>
<view class="text-green-400">我的推荐码可以获得佣金</view>
<!-- #ifdef H5 -->
<view class="my-2 text-center">
<a class="my-2 text-center" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="my-2 text-left text-sm">{{ recommendUrl }}</view>
<!-- #endif -->
</view>
</template>
<script lang="ts" setup>
import { getFooAPI, postFooAPI, IFooItem } from '@/service/foo'
import { IResData } from '@/typings'
const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
onLoad(() => {
getFoo()
postFoo()
})
const originalData = ref<IResData<IFooItem>>()
const data = ref<IFooItem>()
const getFoo = async () => {
const res = await getFooAPI('菲鸽')
data.value = res.result
originalData.value = res
}
const data2 = ref<IFooItem>()
const postFoo = async () => {
const res = await postFooAPI('菲鸽2')
data2.value = res.result
}
const reset = () => {
data.value = undefined
data2.value = undefined
originalData.value = undefined
}
</script>

View File

@ -0,0 +1,145 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '通屏+下拉刷新+自定义导航栏',
enablePullDownRefresh: false,
backgroundColor: '#23c09c', // .top-section
'app-plus': {
titleNView: {
type: 'transparent',
},
},
'mp-weixin': {
navigationStyle: 'custom',
},
},
}
</route>
<template>
<!-- #ifdef MP-WEIXIN -->
<view class="fly-navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<!-- 1/3多于1个页面用返回图标 -->
<navigator v-if="pages.length > 1" open-type="navigateBack" class="left-icon">
<view class="i-carbon-chevron-left text-current"></view>
</navigator>
<!-- 2/3只有1个页面如果不是tabbar需要首页图标 -->
<!-- 这种情况一般出现在用户直接打开分享出去的详情页面或者使用redirectTo等API -->
<navigator
v-else-if="!isTabbar"
open-type="switchTab"
url="/pages/index/index"
class="left-icon"
>
<view class="i-carbon-home text-current"></view>
</navigator>
<!-- 3/3如果当前页就是tabbar页不用去首页也就是什么图标都不需要 -->
<view class="title">{{ '我是标题' }}</view>
</view>
<!-- #endif -->
<scroll-view
enable-back-to-top
scroll-y
class="scroll-view-bg flex-1 h-full"
id="scroller"
refresher-enabled
@scrolltolower="onScrollToLower"
@refresherrefresh="onRefresherRefresh"
:refresher-triggered="isTriggered"
>
<view class="top-section" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<view class="pt-1">顶部区域</view>
<view>可以是标题也可以是个人中心头像等</view>
<view>建议本区域高度不低于200rpx</view>
</view>
<view class="p-2 leading-6 bg-white">
注意上面的导航栏渐变效果仅微信端支持且上面的导航栏无法抽为组件引入使用否则滚动效果没有了如果不只是微信小程序使用可以
onPageScroll 实现全端效果一样另外如果是app端还可以配置 titleNView参考
https://uniapp.dcloud.net.cn/tutorial/page.html#onpagescroll
</view>
<view class="bg-white">
<fly-content :line="30" />
</view>
</scroll-view>
</template>
<script lang="ts" setup>
import useNavbarWeixin from '@/hooks/useNavbarWeixin'
import { onPullDownRefresh } from '@dcloudio/uni-app'
const { pages, isTabbar, onScrollToLower, safeAreaInsets } = useNavbarWeixin()
//
onPullDownRefresh(() => {
setTimeout(function fn() {
console.log('refresh - onPullDownRefresh')
//
uni.stopPullDownRefresh()
}, 1000)
})
//
const isTriggered = ref(false)
//
const onRefresherRefresh = async () => {
//
isTriggered.value = true
setTimeout(function fn() {
console.log('refresh - onRefresherRefresh')
//
isTriggered.value = false
}, 1000)
}
</script>
<style lang="scss">
.scroll-view-bg {
// .top-section
background-color: #23c09c;
}
// 200rpx
.top-section {
display: flex;
flex-direction: column;
align-items: center;
min-height: 200rpx;
padding: 40rpx 0;
line-height: 2;
color: #fff;
background-image: url('https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/top-bg.png');
background-size: cover;
}
.fly-navbar {
position: fixed;
top: 0;
left: 0;
z-index: 9;
width: 750rpx;
color: #000;
background-color: transparent;
.left-icon {
position: absolute;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
font-size: 44rpx;
color: #000;
}
.title {
display: flex;
align-items: center;
justify-content: center;
height: 44px;
font-size: 32rpx;
color: transparent;
}
}
</style>

View File

@ -0,0 +1,16 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: 'UniUI Icons 使用' },
}
</route>
<template>
<view class="m-4">
<uni-icons type="contact" size="30"></uni-icons>
<uni-icons type="contact" size="30" color="red"></uni-icons>
<view class="text-blue-300"
>注意在微信小程序中不支持改颜色即设置了颜色也会变成默认的#333, BUG</view
>
</view>
</template>

View File

@ -0,0 +1,14 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: 'UniUI 使用' },
}
</route>
<template>
<uni-card>
<text>这是一个基础卡片示例内容较少此示例展示了一个没有任何属性不带阴影的卡片</text>
</uni-card>
<view>微信里面下面的 uni-badge 显示不出来BUG</view>
<uni-badge text="99"></uni-badge>
</template>

View File

@ -0,0 +1,20 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: 'UnoCss Icons 使用' },
}
</route>
<template>
<view class="m-4">
<view class="mb-2">
这里只装了carbon的图表库网址
<a href="https://icones.js.org/collection/carbon" target="_blank"
>https://icones.js.org/collection/carbon </a
>(非H5环境请使用浏览器打开)
</view>
<view class="i-carbon-car" />
<view class="i-carbon-car text-red" />
<button class="i-carbon-sun dark:i-carbon-moon" />
</view>
</template>

View File

@ -0,0 +1,15 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: 'UnoCss 使用' },
}
</route>
<template>
<view class="flex flex-col justify-center items-center text-5 h-8 leading-8 mt-20">
<view class="text-green-500">文字颜色 text-light-50</view>
<view class="text-red-500">文字颜色 text-red-500</view>
<view class="bg-green-500">背景色 bg-light-50</view>
<view class="bg-red-500">背景色 bg-red-500</view>
</view>
</template>

View File

@ -0,0 +1,22 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '开启 vConsole' },
}
</route>
<template>
<view class="text-5 h-8 leading-8">
<view class="text-red-500">在非正式版小程序里面已经集成了VConsole</view>
<view class="text-blue-500 mb-2">开启方式如下面</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/vconsole/1.png"
mode="widthFix"
/>
<view class="text-blue-500 m-2">然后页面上会出现一个 `vConsole` 的调试按钮如下图</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/vconsole/2.png"
mode="widthFix"
/>
</view>
</template>

81
src/pages/demo/index.vue Normal file
View File

@ -0,0 +1,81 @@
<template>
<view class="bg-slate-100 p-4">
<view class="bg-slate-100 w-full" v-for="item in listData" :key="item.id">
<view class="font-800">{{ item.title }}</view>
<view v-for="itemDetail in item.list" :key="itemDetail.path" class="mt-3">
<view
class="flex bg-white items-center justify-between p-3 mb-2"
@click="goDetailPage(itemDetail.path)"
>
<text class="flex-1 text-4 text-dark">{{ itemDetail.title }}</text>
<text class="i-carbon-chevron-right"></text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts" name="TestIndex">
import pagesJson from '@/pages.json'
/** 基本功能 */
const baseDemos = pagesJson.pages
.filter((e) => e.path.startsWith('pages/demo/base'))
.map((e) => ({
title: e.style?.navigationBarTitleText || '默认页面标题',
path: e.path,
}))
/** 页面功能 */
const pageDemos = pagesJson.pages
.filter((e) => e.path.startsWith('pages/demo/page'))
.map((e) => ({
title: e.style?.navigationBarTitleText || '默认页面标题',
path: e.path,
}))
const listData = reactive([
{
id: 1,
title: '基础功能',
list: baseDemos,
},
{
id: 2,
title: '页面功能',
list: pageDemos,
},
])
const goDetailPage = (path: string) => {
const url = `/${path}`
uni.navigateTo({
url,
})
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
width: 200rpx;
height: 200rpx;
margin: 200rpx auto 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>

View File

@ -0,0 +1,130 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '动态时钟' },
}
</route>
<template>
<view class="mt-4 h-10 text-center">动态时钟</view>
<view class="clock-box">
<view class="clock" :style="{ '--ds': ds, '--dm': dm, '--dh': dh }">
<view class="clock-pane">
<text class="clock-num" :style="{ '--i': n }" v-for="n in 12" :key="n">{{ n }}</text>
</view>
<view class="clock-hour"></view>
<view class="clock-min"></view>
<view class="clock-sec"></view>
</view>
</view>
</template>
<script lang="ts" setup>
const d = new Date()
const h = d.getHours()
const m = d.getMinutes()
const s = d.getSeconds()
const ds = ref(s)
const dm = ref(m + s / 60)
const dh = ref(h + m / 60 + s / 3600)
</script>
<style lang="scss">
.clock-box {
display: flex;
align-items: center;
justify-content: center;
}
.clock {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 350px;
height: 350px;
font-size: 24px;
border-radius: 20px;
box-shadow: 2px 2px 20px #0000001a;
--step: 60s;
}
.clock::before {
position: absolute;
width: 300px;
height: 300px;
content: '';
background: repeating-conic-gradient(from -0.5deg, #333 0 1deg, transparent 0deg 30deg),
repeating-conic-gradient(from -0.5deg, #ccc 0 1deg, transparent 0deg 6deg);
border-radius: 50%;
mask: radial-gradient(transparent 145px, red 0);
}
.clock-pane {
position: absolute;
width: 250px;
height: 250px;
transform: translateX(-125px);
}
.clock-num {
position: absolute;
offset-path: path(
'M250 125c0 69.036-55.964 125-125 125S0 194.036 0 125 55.964 0 125 0s125 55.964 125 125z'
);
offset-distance: calc(var(--i) * 10% / 1.2 - 25%);
offset-rotate: 0deg;
}
.clock-hour {
position: absolute;
width: 4px;
height: 60px;
background: #333;
transform: translateY(-50%) rotate(0);
transform-origin: center bottom;
animation: clock calc(var(--step) * 60 * 12) infinite linear;
animation-delay: calc(-1 * var(--step) * var(--dh) * 60);
}
.clock-min {
position: absolute;
width: 4px;
height: 90px;
background: #333;
transform: translateY(-50%) rotate(0);
transform-origin: center bottom;
animation: clock calc(var(--step) * 60) infinite linear;
animation-delay: calc(-1 * var(--step) * var(--dm));
}
.clock-sec {
position: absolute;
width: 2px;
height: 120px;
background: red;
transform: translateY(-50%) rotate(0);
transform-origin: center bottom;
animation: clock var(--step) infinite steps(60);
animation-delay: calc(-1 * var(--step) * var(--ds) / 60);
}
.clock-sec::after {
position: absolute;
bottom: 0;
left: 50%;
width: 10px;
height: 10px;
content: '';
background: #fff;
border: 4px solid #333;
border-radius: 50%;
transform: translate(-50%, 50%);
}
@keyframes clock {
to {
transform: translateY(-50%) rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,152 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '动态时钟-抗锯齿' },
}
</route>
<template>
<view class="mt-4 h-10 text-center">动态时钟</view>
<view class="clock-box">
<view class="clock" :style="{ '--ds': ds, '--dm': dm, '--dh': dh }">
<view class="clock-pane">
<text class="clock-num" :style="{ '--i': n }" v-for="n in 12" :key="n">{{ n }}</text>
</view>
<view class="clock-scales">
<text class="clock-scale" :style="{ '--i': n }" v-for="n in 60" :key="n"></text>
</view>
<view class="clock-hour"></view>
<view class="clock-min"></view>
<view class="clock-sec"></view>
</view>
</view>
</template>
<script lang="ts" setup>
const d = new Date()
const h = d.getHours()
const m = d.getMinutes()
const s = d.getSeconds()
const ds = ref(s)
const dm = ref(m + s / 60)
const dh = ref(h + m / 60 + s / 3600)
</script>
<style lang="scss">
.clock-box {
display: flex;
align-items: center;
justify-content: center;
}
.clock {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 350px;
height: 350px;
font-size: 24px;
border-radius: 20px;
box-shadow: 2px 2px 20px #0000001a;
--step: 60s;
}
.clock-pane {
position: absolute;
width: 250px;
height: 250px;
transform: translateX(-125px);
}
.clock-scales {
position: absolute;
width: 250px;
height: 250px;
transform: translate(125px, -25px);
}
.clock-scale {
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 4px;
background: #ccc;
transform-origin: 0 150px;
&:nth-child(5n + 1) {
width: 4px;
height: 6px;
background: #333;
}
}
@for $i from 1 through 60 {
.clock-scale:nth-child(#{$i}) {
transform: rotate(#{($i - 1) * 6deg});
}
}
.clock-num {
position: absolute;
offset-path: path(
'M250 125c0 69.036-55.964 125-125 125S0 194.036 0 125 55.964 0 125 0s125 55.964 125 125z'
);
offset-distance: calc(var(--i) * 10% / 1.2 - 25%);
offset-rotate: 0deg;
}
.clock-hour {
position: absolute;
width: 4px;
height: 60px;
background: #333;
transform: translateY(-50%) rotate(0);
transform-origin: center bottom;
animation: clock calc(var(--step) * 60 * 12) infinite linear;
animation-delay: calc(-1 * var(--step) * var(--dh) * 60);
}
.clock-min {
position: absolute;
width: 4px;
height: 90px;
background: #333;
transform: translateY(-50%) rotate(0);
transform-origin: center bottom;
animation: clock calc(var(--step) * 60) infinite linear;
animation-delay: calc(-1 * var(--step) * var(--dm));
}
.clock-sec {
position: absolute;
width: 2px;
height: 120px;
background: red;
transform: translateY(-50%) rotate(0);
transform-origin: center bottom;
animation: clock var(--step) infinite steps(60);
animation-delay: calc(-1 * var(--step) * var(--ds) / 60);
}
.clock-sec::after {
position: absolute;
bottom: 0;
left: 50%;
width: 10px;
height: 10px;
content: '';
background: #fff;
border: 4px solid #333;
border-radius: 50%;
transform: translate(-50%, 50%);
}
@keyframes clock {
to {
transform: translateY(-50%) rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,88 @@
<route lang="json5">
{
layout: 'default',
style: { navigationBarTitleText: '页面悬浮球' },
}
</route>
<template>
<view>
<movable-area class="movable-area">
<movable-view
:style="`--size:${ballSize}px`"
class="movable-view"
direction="all"
:x="x"
:y="y"
@change="onChange"
@touchend.prevent="onTouchEnd"
>
<view class="w-full h-full rounded-full bg-green-400"></view>
</movable-view>
</movable-area>
<view>页面其他元素</view>
<view>可以正常触发点击事件吗答案是可以的</view>
<button @click="onClick">按钮</button>
<view>{{ x }}</view>
<view>{{ y }}</view>
<view @click="onSet">点击设置</view>
</view>
</template>
<script lang="ts" setup name="FloatingBubble">
const { windowHeight, windowWidth } = uni.getSystemInfoSync()
const ballSize = 60
const x = ref(windowWidth - ballSize) //
const y = ref(windowHeight - ballSize - 20) // 20px
const middleX = (windowWidth - ballSize) / 2
const onChange: UniHelper.MovableViewOnChange = (e) => {
const { x: _x, y: _y } = e.detail
x.value = _x
y.value = _y
}
// TODO:
const onTouchEnd = (e) => {
console.log('onTouchEnd', e)
// TODOonSetonSet
//
const tid = setTimeout(() => {
if (x.value < middleX) {
x.value = 0
} else {
x.value = windowWidth - ballSize
}
clearTimeout(tid)
}, 0)
}
const onClick = () => {
uni.showToast({
title: 'yes',
icon: 'none',
})
}
const onSet = () => {
x.value = 100
y.value = 100
}
</script>
<style lang="scss">
.movable-area {
position: absolute;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: 100%;
pointer-events: none; // area便
.movable-view {
width: var(--size);
height: var(--size);
pointer-events: auto; //
}
}
</style>

View File

@ -0,0 +1,83 @@
<route lang="json5">
{
layout: 'demo',
style: {
navigationBarTitleText: '%app.name%',
},
}
</route>
<template>
<view class="center flex-col mt-6">
<view class="text-green-500">多语言测试</view>
<view class="m-4">{{ $t('app.name') }}</view>
<view class="text-green-500 mt-12">切换语言 </view>
<view class="uni-list">
<radio-group @change="radioChange" class="radio-group">
<label class="uni-list-cell uni-list-cell-pd" v-for="item in languages" :key="item.value">
<view>
<radio :value="item.value" :checked="item.value === current" />
</view>
<view>{{ item.name }}</view>
</label>
</radio-group>
</view>
<!-- http://localhost:9100/#/pages/index/i18n -->
<button @click="testI18n" class="mt-20 mb-44">测试弹窗</button>
</view>
</template>
<script lang="ts" setup>
import i18n from '@/locale/index'
import { testI18n } from '@/utils/index'
const current = ref(uni.getLocale())
const languages = [
{
value: 'zh-Hans',
name: '中文',
checked: 'true',
},
{
value: 'en',
name: '英文',
},
]
const radioChange = (evt) => {
// console.log(evt)
current.value = evt.detail.value
// 2
uni.setLocale(evt.detail.value)
i18n.global.locale = evt.detail.value
}
</script>
<style lang="scss">
.uni-list {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
background-color: #fff;
border-radius: 12px;
}
.radio-group {
width: 200px;
margin: 10px auto;
border-radius: 12px;
}
.uni-list-cell {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 10px;
background-color: #bcecd1;
}
</style>

View File

@ -0,0 +1,29 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '图片压缩' },
}
</route>
<template lang="">
<view class="m-4 text-center">
<view class="mb-2 text-orange-500">
原始图片是一个很大的2.5Mbuild之后生成的图片只有1.1M体积下降 56%
</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/img-min/bg-1.png"
mode="scaleToFill"
/>
<view class="mb-4">对比图如下2图如果看不清请看代码原图</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/img-min/before.png"
mode="widthFix"
class="w-full"
/>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/img-min/after.png"
mode="widthFix"
class="w-full"
/>
</view>
</template>

View File

@ -0,0 +1,127 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '九宫格抽奖' },
}
</route>
<template>
<view class="mt-4 h-10 text-center">九宫格抽奖</view>
<view class="lottery-box">
<view class="lottery-list">
<view
class="lottery-item"
:class="{
active: n === activeIndex,
btn: n === btnIndex, //
}"
v-for="n in numList"
:key="n"
@click="handleClick(n)"
>
<view v-if="n === btnIndex">点击抽奖</view>
<view v-else> {{ n }}</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { reactive, computed } from 'vue'
const giftLen = 8 // 8
const loop = 4 //
const totalStep = giftLen * loop // 32
const lastLoopStep = totalStep - giftLen // 24
const numList = [1, 2, 3, 8, -1, 4, 7, 6, 5]
const btnIndex = numList[4] //
const state = reactive({
lottery: 0, //
step: -1, //
stopStep: totalStep, //
speed: 2, //
timer: null, // ID
loading: false,
})
// 8
const activeIndex = computed(() => {
return (state.step % 8) + 1
})
function run() {
//
if (state.step >= state.stopStep) {
//
clearTimeout(state.timer)
//
state.step = state.lottery
state.speed = 2
state.loading = false
console.log(`恭喜获得${activeIndex.value}号奖品`)
uni.showModal({
title: `恭喜获得${activeIndex.value}号奖品`,
})
return
}
// speed
if (state.step > lastLoopStep + state.lottery) {
state.speed++
}
//
state.step++
//
state.timer = setTimeout(run, state.speed * 30)
}
//
function handleClick(n) {
if (n !== btnIndex) {
return
}
if (state.loading) return
state.loading = true
// 使
state.lottery = Math.ceil(Math.random() * giftLen)
console.log(state.lottery)
// 4
state.stopStep = state.lottery + totalStep
//
run()
}
</script>
<style lang="css">
.lottery-box {
display: flex;
align-items: center;
justify-content: center;
}
.lottery-list {
--size: 100px;
display: flex;
flex-wrap: wrap;
width: calc(3 * var(--size) + 3px);
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
.lottery-item {
width: var(--size);
height: var(--size);
line-height: var(--size);
text-align: center;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
.lottery-item.active {
color: #fff;
background-color: red;
}
.lottery-item.btn {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,216 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '大转盘抽奖' },
}
</route>
<template>
<view class="text-center">
<view class="container">
<view class="prize-list" :style="styleObj">
<view
class="prize-item"
v-for="(item, index) in prizeList"
:key="item.id"
:style="prizeStyle(index)"
>
<image :src="item.pic" class="gift-img" />
<text class="gift-name">{{ item.name }}</text>
</view>
</view>
<view class="lottery-btn" @click="start"> </view>
</view>
<view class="text-blue-600 my-2">目标是实现如下的效果但是我感觉只用css还是太难了</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery/target.png"
mode="widthFix"
width="552px"
/>
<!-- <image :src="targetImg" mode="widthFix" width="552px" /> -->
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
// TODO: fix
// import targetImg from 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery/target.png'
//
const prizeList = [
{
id: 0,
name: '双肩包',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/backpack.jpg',
},
{
id: 1,
name: '积木',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/jimu.jpg',
},
{
id: 2,
name: '红包',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/red-envelope.jpg',
},
{
id: 3,
name: '茶具',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/tea-set.jpg',
},
{
id: 4,
name: '可爱脸',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/tushetou.jpg',
},
{
id: 5,
name: '挖掘机',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/wajueji.jpg',
},
{
id: 6,
name: '无辜脸',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/xiaolian.jpg',
},
{
id: 7,
name: '烟灰缸',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/yanhuigang.jpg',
},
]
let isRunning = false //
const baseRunAngle = 360 * 5 // 5
let prizeId = 0 // id
//
const rotateAngle = computed(() => {
const _degree = 360 / prizeList.length
return _degree
})
//
const totalRunAngle = ref(baseRunAngle - (prizeId + 0.5) * rotateAngle.value)
//
const bgColor = (() => {
const [c1, c2] = ['#5352b3', '#363589']
// repeating-conic-gradient(red 0 15deg, blue 15deg 30deg);
return `background: repeating-conic-gradient(${c1} 0 ${rotateAngle.value}deg,
${c2} ${rotateAngle.value}deg ${2 * rotateAngle.value}deg);`
})()
const styleObj = ref(bgColor)
//
const prizeStyle = computed(() => {
const _degree = rotateAngle.value
return (i) => {
// 90vw45vw
return `
width: ${2 * 45 * Math.sin(((_degree / 2) * Math.PI) / 180)}vw;
height: 45vw;
transform: rotate(${_degree * i + _degree / 2}deg);
transform-origin: 50% 100%;
`
}
})
//
const getRandomNum = () => {
const num = Math.floor(Math.random() * prizeList.length)
return num
}
const stopRun = () => {
isRunning = false
const prizeName = prizeList.find((e) => e.id === prizeId)!.name
uni.showModal({
title: `恭喜你中奖 ${prizeName}`,
success() {
styleObj.value = `${bgColor} transform: rotate(0deg);`
},
})
}
const startRun = () => {
console.log(isRunning, totalRunAngle.value)
//
styleObj.value = `${bgColor} transform: rotate(${totalRunAngle.value}deg); transition: all 4s ease;`
setTimeout(stopRun, 4000)
}
const start = () => {
if (!isRunning) {
isRunning = true
console.log('开始抽奖,后台请求中奖奖品')
// 使
prizeId = getRandomNum()
totalRunAngle.value = baseRunAngle - (prizeId + 0.5) * rotateAngle.value
console.log('中奖ID>>>', prizeId, prizeList[prizeId], totalRunAngle.value)
nextTick(() => {
startRun()
})
}
}
</script>
<style lang="scss">
.container {
position: relative;
width: 90vw;
height: 90vw;
margin: 20px auto;
border: 10px solid #98d3fc;
border-radius: 50%;
}
.prize-list {
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 50%;
// 使outlineborder
// outline: 10px solid #98d3fc;
}
.prize-item {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
flex-direction: column;
margin: auto;
// border: 2px solid red;
}
.prize-item .gift-img {
display: block;
width: 30%;
height: 20%;
margin: 20px auto 10px;
border-radius: 50%;
}
.prize-item .gift-name {
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
}
.lottery-btn {
position: absolute;
top: 50%;
left: 50%;
width: 80px;
height: 96px;
margin: auto;
cursor: pointer;
background: url('https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/btn-enable.png')
no-repeat center / 100% 100%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -0,0 +1,196 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '九宫格抽奖' },
}
</route>
<template>
<view>
<view class="container">
<view
class="gift-item"
:class="{ active: currentIndex === index }"
v-for="(item, index) in prizeList"
:key="index"
@click="start(index)"
>
<image :src="item.pic" class="gift-img" />
<text v-if="index !== 4" class="gift-name">{{ item.name }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
const currentIndex = ref(0) //
//
const prizeList = [
{
id: 0,
name: '双肩包',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/backpack.jpg',
},
{
id: 1,
name: '积木',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/jimu.jpg',
},
{
id: 2,
name: '红包',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/red-envelope.jpg',
},
{
id: 3,
name: '茶具',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/tea-set.jpg',
},
{
id: 5,
name: '可爱脸',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/tushetou.jpg',
},
{
id: 6,
name: '挖掘机',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/wajueji.jpg',
},
{
id: 7,
name: '无辜脸',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/xiaolian.jpg',
},
{
id: 8,
name: '烟灰缸',
pic: 'https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery-prize/yanhuigang.jpg',
},
]
const startBtn = {
id: 4,
name: '开始按钮',
pic: 'https://img2.baidu.com/it/u=1497996119,382735686&fm=253',
}
//
prizeList.splice(4, 0, startBtn)
//
const prizeSort = [0, 1, 2, 5, 8, 7, 6, 3]
//
const getRandomNum = () => prizeSort[Math.floor(Math.random() * prizeSort.length)]
let isRunning = false //
let speed = 10 //
let timerIns = null //
let currentRunCount = 0 //
const totalRunCount = 32 // 8
let prizeId = 0 // id(0-84)
//
const totalRunStep = computed(() => {
return totalRunCount + prizeSort.indexOf(prizeId)
})
const stopRun = () => {
// eslint-disable-next-line no-unused-expressions
timerIns && clearTimeout(timerIns)
}
const startRun = () => {
stopRun()
console.log(currentRunCount, totalRunStep.value)
//
//
if (currentRunCount > totalRunStep.value) {
isRunning = false
const prizeName = prizeList.find((e) => e.id === prizeId)!.name
uni.showModal({
title: `恭喜你中奖 ${prizeName}`,
})
return
}
currentIndex.value = prizeSort[currentRunCount % 8]
// 2/3
if (currentRunCount > Math.floor((totalRunCount * 2) / 3)) {
speed += Math.floor(currentRunCount / 3)
console.log('速度>>>>', speed)
}
timerIns = setTimeout(() => {
currentRunCount++
startRun()
}, speed)
}
const start = (i) => {
if (i === 4 && !isRunning) {
//
currentRunCount = 0
speed = 100
isRunning = true
console.log('开始抽奖,后台请求中奖奖品')
// 使 4
// const prizeId = getRandomNum()
// console.log('ID>>>', prizeId, prizeList[prizeId])
// prizeId = prizeId
// stopRun()
setTimeout(() => {
prizeId = getRandomNum()
console.log('中奖ID>>>', prizeId, prizeList[prizeId])
//
}, 2000)
startRun()
}
}
</script>
<style lang="scss">
.container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
width: 90vw;
height: 90vw;
margin: 20px auto;
background: #98d3fc;
border: 1px solid #98d3fc;
}
.gift-item {
position: relative;
box-sizing: border-box;
width: 30vw;
height: 30vw;
border: 2px solid #fff;
}
.gift-item:nth-of-type(5) {
cursor: pointer;
}
.gift-item .gift-img {
width: 100%;
height: 100%;
}
.gift-item .gift-name {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 20px;
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
background: rgb(0 0 0 / 50%);
}
.active {
border: 2px solid red;
box-shadow: 2px 2px 30px #fff;
}
</style>

View File

@ -0,0 +1,181 @@
<route lang="json5">
{
layout: 'demo',
style: { navigationBarTitleText: '大转盘抽奖' },
}
</route>
<template>
<view class="mt-4 h-10 text-center">大转盘抽奖</view>
<div class="lottery-box">
<div class="lottery-list">
<div class="lottery-item" v-for="(n, index) in giftLen" :key="n">
<div class="lottery-item-inner">
<div class="lottery-item-gift">奖品{{ index + 1 }}</div>
</div>
</div>
<div
class="pointer"
@click="handleClick"
:style="{ transform: `rotate(${state.stopDeg}deg)` }"
>
<div>开始</div>
<div>抽奖</div>
</div>
</div>
</div>
<view class="leading-8">
<view class="mt-8 text-center text-green-600">下面是调试过程图片</view>
<view class="mb-8 text-center text-green-600">欢迎感兴趣的玩家继续优化</view>
<view class="text-center text-blue-600">计算lottery-item-inner节点的padding-left值</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery/lottery2-1.png"
mode="widthFix"
class="w-full"
/>
<view class="text-center text-blue-600">调整lottery-item-gift节点</view>
<image
src="https://cip-shopping-page-0eysug01066a9e-1302818703.tcloudbaseapp.com/fly/lottery/lottery2-2.png"
mode="widthFix"
class="w-full"
/>
</view>
</template>
<script lang="ts" setup>
const giftLen = 8
const deg = 360 / giftLen //
const loop = 4 // 4
const state = reactive({
lottery: 0, //
lastLottery: 0, //
stopDeg: 0, //
loading: false,
})
function handleClick() {
if (state.loading) return
state.loading = true
// 使0~9
state.lottery = Math.floor(Math.random() * giftLen)
console.log(state.lottery)
// +0+
state.stopDeg += (state.lottery + (giftLen - state.lastLottery)) * deg + loop * 360
// uniaddEventListener
setTimeout(() => {
state.lastLottery = state.lottery
state.loading = false
// alert(`${state.lottery + 1}`)
uni.showModal({
title: `恭喜获得奖品${state.lottery + 1}`,
})
}, 3000)
}
</script>
<style lang="scss">
.lottery-box {
display: flex;
align-items: center;
justify-content: center;
}
.lottery-list {
--size: 600rpx;
--half: calc(var(--size) / 2);
--len: 8; // giftLen
--deg: calc(360 / var(--len) * 1deg);
--deg-num: calc(360 / var(--len));
position: relative;
width: var(--size);
height: var(--size);
border: 2px solid #f55;
border-radius: 50%;
}
.lottery-item {
position: absolute;
top: 0;
left: var(--half);
width: var(--half);
height: var(--size);
overflow: hidden; //
// background-color: #ff5350a1; //
transform-origin: left center;
}
.lottery-item-inner {
position: absolute;
top: 0;
left: calc(-1 * var(--half));
box-sizing: border-box;
width: var(--half);
height: var(--size);
padding-left: calc(((1 - sin(var(--deg-num))) * var(--size)));
font-size: 12px;
border-radius: var(--half) 0 0 var(--half);
transform: rotate(var(--deg));
transform-origin: right center;
}
.lottery-item-inner .lottery-item-gift {
display: block;
text-align: center;
transform: rotate(calc(-0.5 * var(--deg))) translateY(16px)
translateX(calc(0.5 * var(--half) * (1 - 1 / cos(0.5 * var(--deg)))));
transform-origin: center;
}
.lottery-item:nth-child(2n + 1) .lottery-item-inner {
background: #fef6e0a1;
}
.lottery-item:nth-child(2n) .lottery-item-inner {
background: #ffffffa1;
}
// TIPS: --len
@for $i from 1 through 8 {
.lottery-item:nth-child(#{$i}) {
transform: rotate(calc(($i - 1 - 0.5) * var(--deg)));
}
}
.pointer {
--pointer-size: 40px;
--pointer-padding: calc(var(--pointer-size) / 5);
position: absolute;
top: calc(var(--half) - var(--pointer-size) / 2 - var(--pointer-padding));
left: calc(var(--half) - var(--pointer-size) / 2 - var(--pointer-padding));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: var(--pointer-size);
height: var(--pointer-size);
padding: var(--pointer-padding);
font-size: 12px;
text-align: center;
background-color: #ffffffd1;
border: 1px solid #ff5350;
border-radius: 50%;
transition: transform 3s cubic-bezier(0.2, 0.93, 0.43, 1);
}
.pointer::after {
--caret-size: 8px;
position: absolute;
bottom: calc(var(--pointer-size) + var(--pointer-padding) * 2);
left: calc(var(--pointer-size) / 2 - var(-caret-size) / 2);
content: '';
border-color: transparent;
border-style: solid;
border-width: calc(var(--caret-size) * 2) var(--caret-size);
border-bottom-color: #ff5350;
transform-origin: center;
}
</style>

View File

@ -0,0 +1,266 @@
<route lang="json5">
{
layout: 'default',
style: { navigationBarTitleText: '签字板' },
}
</route>
<template>
<view class="canvas-box flex flex-col box-border p-3" :class="{ 'full-screen': isFullScreen }">
<canvas
canvas-id="canvas"
class="w-full b b-dashed b-rd b-gray-300 canvas"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
@mousedown="touchStart"
@mousemove="touchMove"
@mouseup="touchEnd"
/>
<view class="btns flex justify-between text-center box-border">
<view class="btn-box flex">
<view class="btn bg-gray-100 b-rd b-gray-300 c-gray-500 p-1" @click="handFullScreen">
{{ isFullScreen ? '退出全屏' : '全屏' }}
</view>
<view class="btn bg-gray-100 b-rd b-gray-300 c-gray-500 p-1 ml-2" @click="clear">清空</view>
<view class="btn bg-gray-100 b-rd b-gray-300 c-gray-500 p-1 ml-2" @click="withdraw">
撤回
</view>
</view>
<view class="btn-box flex">
<view class="btn bg-sky-500 b-rd c-white p-1" @click="save">保存</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup name="sign">
const isFullScreen = ref(false)
const isSigned = ref(false)
let ctx = null
let isButtonDown = false
let points = []
let allPoints = []
//
function initCanvas() {
ctx = uni.createCanvasContext('canvas')
//
ctx.lineWidth = 4
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
}
//
function onResize() {
initCanvas() // canvas
}
//
function draw(w?) {
const point1 = points[0]
const point2 = points[1]
if (!w) {
allPoints[allPoints.length - 1].push(JSON.parse(JSON.stringify(points)))
}
points.shift()
ctx.moveTo(point1.X, point1.Y)
ctx.lineTo(point2.X, point2.Y)
ctx.stroke()
ctx.draw(true)
isSigned.value = true
}
//
function touchStart() {
allPoints.push([])
ctx.beginPath() //
isButtonDown = true
}
//
function touchMove(e) {
if (isButtonDown) {
let movePoint = {}
if (e.changedTouches[0].x) {
movePoint = { X: e.changedTouches[0].x, Y: e.changedTouches[0].y }
} else {
const X = e.changedTouches[0].pageX - e.currentTarget.offsetLeft
const Y = e.changedTouches[0].pageY - e.currentTarget.offsetTop
movePoint = { X, Y }
}
points.push(movePoint) //
const len = points.length
if (len >= 2) {
draw() //
}
}
}
//
function touchEnd() {
allPoints = allPoints.filter((e) => {
return e.length > 0
})
points = []
isButtonDown = false
}
// , true
function clear(reset?: boolean) {
if (reset) allPoints = []
ctx.clearRect(0, 0, 1000, 1000)
ctx.draw(true)
isSigned.value = false
}
//
function handFullScreen() {
clear(true)
isFullScreen.value = !isFullScreen.value
const tid = setTimeout(() => {
onResize()
clearTimeout(tid)
}, 100)
}
//
function withdraw() {
//
clear()
if (allPoints.length <= 1) {
allPoints = []
points = []
return
}
//
allPoints.pop()
//
allPoints.forEach((e) => {
e.forEach((r) => {
points = JSON.parse(JSON.stringify(r))
draw(1)
})
})
}
// h5
function saveCanvasAsImage(dataURL, imageName?) {
// Image
const img = new Image()
// imgsrcURL
img.src = dataURL
//
const link = document.createElement('a')
//
link.download = imageName || 'canvas-image'
//
link.href = img.src
link.click()
}
//
const save = () => {
if (!isSigned.value) {
uni.showToast({
title: '请签名',
icon: 'none',
})
return
}
uni.canvasToTempFilePath({
canvasId: 'canvas',
success: (res) => {
//
const { tempFilePath } = res
//
// #ifdef H5
const name = `sign-${new Date().getTime()}`
saveCanvasAsImage(tempFilePath, name)
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.showToast({
title: '图片保存成功',
})
},
fail: (err) => {
console.error(err)
uni.showToast({
title: '图片保存失败',
icon: 'none',
})
},
})
// #endif
},
fail: () => {
uni.showToast({
title: '转换图片失败',
icon: 'none',
})
},
})
}
onMounted(() => {
initCanvas()
})
</script>
<style lang="scss" scoped>
$padding: 30rpx;
.canvas-box {
.canvas {
height: 300rpx;
transition: height 0.3s;
}
.btns {
margin-top: 10rpx;
transition: transform 0.3s;
.btn {
width: auto;
height: 50rpx;
}
}
}
.full-screen {
flex-direction: row;
height: calc(100vh - 88rpx);
.canvas {
width: calc(100% - 100rpx);
height: 100%;
margin-left: 100rpx;
}
.btns {
position: absolute;
align-items: center;
width: calc(100vh - (88rpx + $padding * 2));
height: 100rpx;
transform: translate(100rpx, 0) rotate(90deg);
transform-origin: top left;
.btn-box {
flex-direction: row;
}
}
}
</style>

View File

@ -0,0 +1,39 @@
<route lang="json5">
{
style: { navigationBarTitleText: '登录' },
}
</route>
<template>
<view class="p-4">
<view class="flex items-center leading-6" v-if="hasLogin">
<image class="w-8 h-8 rounded-full" :src="userStore.userInfo?.avatar"></image>
<view class="ml-2">{{ userStore.userInfo?.nickname }}</view>
</view>
<view class="flex items-center leading-6" v-else @click="show = true">
<view class="i-carbon-user-avatar"></view>
<view class="ml-2">点击显示微信头像</view>
</view>
<fly-login v-model="show" />
<fly-content :line="10" />
<button v-if="hasLogin" class="mt-2" @click="logout">退出登录</button>
</view>
</template>
<script lang="ts" setup name="WxLogin">
import { useUserStore } from '@/store'
const show = ref(false)
const userStore = useUserStore()
const hasLogin = computed(() => userStore.userInfo?.nickname)
const logout = () => {
uni.showModal({
title: '确认退出当前账号?',
success: (res) => {
if (res.confirm) {
userStore.clearUserInfo()
}
},
})
}
</script>

35
src/pages/my/index.vue Normal file
View File

@ -0,0 +1,35 @@
<route lang="json5">
{
style: { navigationBarTitleText: '我的' },
}
</route>
<template>
<view class="ml-4">wx的openid: </view>
<view class="ml-4">{{ openId }}</view>
<wx-login />
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store'
import { http } from '@/utils/http'
import WxLogin from './components/wx-login.vue'
const userStore = useUserStore()
const openId = ref('')
// openId
uni.login({
provider: 'weixin',
success: async ({ code }) => {
const res = await http<{ session_key: string; openid: string }>({
method: 'GET',
url: '/weixin/jscode2session',
data: {
code,
},
})
openId.value = res.result.openid
userStore.setUserInfo({ openid: res.result.openid })
},
})
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/static/tabbar/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

28
src/store/count.ts Normal file
View File

@ -0,0 +1,28 @@
// src/store/useCountStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCountStore = defineStore(
'count',
() => {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = 0
}
return {
count,
decrement,
increment,
reset,
}
},
{
persist: true,
},
)

View File

@ -16,3 +16,4 @@ export default store
// 模块统一导出
export * from './user'
export * from './count'

View File

@ -1,24 +1,30 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { UserInfo } from '../typings'
import { IUserInfo } from '../typings'
const initState = { nickname: '', avatar: '' }
export const useUserStore = defineStore(
'user',
() => {
const userInfo = ref<UserInfo>({ nickname: '', avatar: '' })
const userInfo = ref<IUserInfo>({ ...initState })
const setUserInfo = (val: UserInfo) => {
const setUserInfo = (val: IUserInfo) => {
userInfo.value = val
}
const clearUserInfo = () => {
userInfo.value = undefined
}
const reset = () => {
userInfo.value = { ...initState }
}
return {
userInfo,
setUserInfo,
clearUserInfo,
reset,
}
},
{

7
src/typings.d.ts vendored
View File

@ -6,15 +6,10 @@ export type IResData<T> = {
result: T
}
export type UserInfo = {
export type IUserInfo = {
nickname?: string
avatar?: string
/** 微信的 openid非微信没有这个字段 */
openid?: string
token?: string
}
export type UserItem = {
username: string
age: number
}

View File

@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import qs from 'qs'
import { useUserStore } from '@/store'
import { IResData, UserInfo } from '@/typings'
import { IResData, IUserInfo } from '@/typings'
type CustomRequestOptions = UniApp.RequestOptions & { query?: Record<string, any> }
@ -36,7 +36,7 @@ const httpInterceptor = {
}
// 4. 添加 token 请求头标识
const userStore = useUserStore()
const { token } = userStore.userInfo as unknown as UserInfo
const { token } = userStore.userInfo as unknown as IUserInfo
if (token) {
options.header.Authorization = `Bearer ${token}`
}

View File

@ -1,5 +1,18 @@
import pagesJson from '@/pages.json'
import { translate as t } from '@/locale/index'
console.log(pagesJson)
/** 判断当前页面是否是tabbar页 */
export const getIsTabbar = () => {
if (!Object.keys(pagesJson).includes('tabBar')) {
return false
}
const pages = getCurrentPages()
const currPath = pages.at(-1).route
return !!pagesJson.tabBar.list.find((e) => e.pagePath === currPath)
}
/**
* test i18n in not .vue file
*/