初始化

This commit is contained in:
2025-12-09 09:06:06 +08:00
commit 2c3e9b248d
98 changed files with 26883 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
<template>
<view class="app-avatar" :style="avatarStyle" @click="onClick">
<wd-img
v-if="src"
:src="src"
:width="sizeRpx"
:height="sizeRpx"
:round="round"
:radius="round ? '50%' : radius"
mode="aspectFill"
/>
<view
v-else
class="avatar-placeholder"
:style="placeholderStyle"
>
{{ displayText }}
</view>
<!-- 徽标 -->
<wd-badge
v-if="badge && badge > 0"
:value="badge"
:max="99"
class="avatar-badge"
/>
<view v-else-if="dot" class="avatar-dot" />
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { generateColor } from '@/utils/format'
interface Props {
src?: string
name?: string
size?: number
round?: boolean
radius?: string
badge?: number
dot?: boolean
bgColor?: string
}
const props = withDefaults(defineProps<Props>(), {
src: '',
name: '',
size: 80,
round: false,
radius: '8rpx',
badge: 0,
dot: false,
bgColor: ''
})
const emit = defineEmits<{
(e: 'click'): void
}>()
const sizeRpx = computed(() => `${props.size}rpx`)
const avatarStyle = computed(() => ({
width: sizeRpx.value,
height: sizeRpx.value
}))
const placeholderStyle = computed(() => ({
width: sizeRpx.value,
height: sizeRpx.value,
borderRadius: props.round ? '50%' : props.radius,
background: props.bgColor || generateColor(props.name),
fontSize: `${props.size * 0.4}rpx`
}))
const displayText = computed(() => {
return props.name?.charAt(0) || '?'
})
function onClick() {
emit('click')
}
</script>
<style lang="scss" scoped>
.app-avatar {
position: relative;
display: inline-block;
}
.avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
}
.avatar-badge {
position: absolute;
top: -8rpx;
right: -8rpx;
}
.avatar-dot {
position: absolute;
top: 0;
right: 0;
width: 16rpx;
height: 16rpx;
background: var(--color-danger);
border-radius: 50%;
border: 2rpx solid var(--bg-content);
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<view class="app-empty">
<wd-status-tip :image="image" :tip="description" />
<slot />
</view>
</template>
<script setup lang="ts">
interface Props {
image?: string
description?: string
}
withDefaults(defineProps<Props>(), {
image: 'content',
description: '暂无数据'
})
</script>
<style lang="scss" scoped>
.app-empty {
padding: 80rpx 0;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<view class="app-loading" :class="{ fullscreen }">
<wd-loading :type="type" :size="size" :color="color" />
<text v-if="text" class="loading-text">{{ text }}</text>
</view>
</template>
<script setup lang="ts">
interface Props {
text?: string
type?: 'circular' | 'spinner'
size?: string
color?: string
fullscreen?: boolean
}
withDefaults(defineProps<Props>(), {
text: '',
type: 'circular',
size: '48rpx',
color: '',
fullscreen: false
})
</script>
<style lang="scss" scoped>
.app-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx;
gap: 20rpx;
&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-mask);
z-index: 1000;
}
}
.loading-text {
font-size: 28rpx;
color: var(--text-tertiary);
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<view class="app-navbar" :class="{ dark: isDark }">
<view class="navbar-status-bar" />
<view class="navbar-content">
<!-- 左侧 -->
<view class="navbar-left" @click="onLeftClick">
<slot name="left">
<template v-if="showBack">
<wd-icon name="arrow-left" size="44rpx" />
</template>
<template v-else-if="showAvatar">
<wd-img
v-if="avatarUrl"
:src="avatarUrl"
width="64rpx"
height="64rpx"
round
/>
<view v-else class="avatar-placeholder">
{{ avatarText }}
</view>
</template>
</slot>
</view>
<!-- 标题 -->
<view class="navbar-title">
<slot name="title">
<text class="title-text">{{ title }}</text>
</slot>
</view>
<!-- 右侧 -->
<view class="navbar-right" @click="onRightClick">
<slot name="right" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTheme } from '@/composables/useTheme'
interface Props {
title?: string
showBack?: boolean
showAvatar?: boolean
avatarUrl?: string
avatarText?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '',
showBack: false,
showAvatar: false,
avatarUrl: '',
avatarText: '?'
})
const emit = defineEmits<{
(e: 'left-click'): void
(e: 'right-click'): void
}>()
const { isDark } = useTheme()
function onLeftClick() {
if (props.showBack) {
uni.navigateBack()
}
emit('left-click')
}
function onRightClick() {
emit('right-click')
}
</script>
<style lang="scss" scoped>
.app-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: var(--nav-bg);
}
.navbar-status-bar {
height: var(--status-bar-height, 0);
}
.navbar-content {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 30rpx;
}
.navbar-left {
min-width: 80rpx;
color: var(--text-primary);
}
.navbar-title {
flex: 1;
text-align: center;
.title-text {
font-size: 36rpx;
font-weight: 600;
color: var(--text-primary);
}
}
.navbar-right {
min-width: 80rpx;
display: flex;
justify-content: flex-end;
color: var(--text-primary);
}
.avatar-placeholder {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
font-size: 28rpx;
font-weight: 600;
}
</style>