This commit is contained in:
WindowBird 2025-10-30 18:00:30 +08:00
parent 89815a4d96
commit 04568368d7
9 changed files with 480 additions and 69 deletions

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,68 @@
<template>
<view v-if="show" class="modal-mask">
<view class="modal-content">
<view class="modal-title">新建日程</view>
<input v-model="_title" placeholder="请输入标题" class="input" />
<view class="sn">时间<input v-model="_hour" type="number" min="0" max="23" />:<input v-model="_min" type="number" min="0" max="59" /></view>
<view class="sn">
颜色
<input v-model="_color" placeholder="#e3fae6" />
</view>
<view class="buttons">
<button @click="$emit('cancel')">取消</button>
<button @click="ok">确定</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
show: Boolean
});
const emit = defineEmits(['ok', 'cancel']);
const _title = ref('');
const _hour = ref('');
const _min = ref('');
const _color = ref('#e3fae6');
function ok() {
emit('ok', { title:_title.value, startHour:Number(_hour.value), startMin:Number(_min.value), color:_color.value });
_title.value = '';
_hour.value = '';
_min.value = '';
_color.value = '#e3fae6';
}
watch(() => props.show, v => {
if (!v) {
_title.value = '';
_hour.value = '';
_min.value = '';
_color.value = '#e3fae6';
}
});
</script>
<style scoped lang="scss">
.modal-mask {
position: fixed; left: 0; top: 0; right: 0; bottom: 0;
background: rgba(0,0,0,.16);
display: flex; align-items: center; justify-content: center;
z-index: 999;
}
.modal-content {
background: #fff; padding: 32rpx 28rpx; border-radius: 16rpx;
min-width: 440rpx;
}
.modal-title {
color: #2885ff; font-size: 20px; font-weight: 600; margin-bottom: 25rpx;
}
.input {
padding: 12rpx; border-radius: 8rpx; margin: 10rpx 0; border: 1px solid #e3e3e3; width: 90%;
}
.buttons {
margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 30rpx;
}
.sn {
margin-bottom: 14rpx;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<view class="bottom-tabbar">
<view
v-for="item in items"
:key="item.value"
class="tabbar-item"
:class="{active: item.value === modelValue}"
@click="$emit('update:modelValue', item.value)"
>
<uv-icon :name="item.icon" :color="item.value === modelValue ? '#2885ff' : '#888'" size="22" />
<view class="name">{{ item.label }}</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
items: {
type: Array,
required: true
},
modelValue: {
type: [String, Number],
required: true
}
});
const emit = defineEmits(['update:modelValue']);
</script>
<style scoped lang="scss">
.bottom-tabbar {
position: fixed;
left: 0; right: 0; bottom: 0;
display: flex;
height: 104rpx;
background: #fff;
border-top: 1px solid #f0f0f0;
z-index: 99;
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #888;
font-size: 12px;
}
.tabbar-item.active {
color: #2885ff;
}
.name {
margin-top: 7rpx;
}
</style>

81
components/DateBar.vue Normal file
View File

@ -0,0 +1,81 @@
<template>
<scroll-view scroll-x class="date-bar">
<view
v-for="item in days"
:key="item.date"
class="date-item"
:class="{selected: item.date===modelValue, holiday: item.isHoliday, weekend: item.isWeekend}"
@click="$emit('update:modelValue', item.date)"
>
<view class="week">{{ item.weekStr }}</view>
<view class="date">{{ item.day }}</view>
<view class="tips" v-if="item.tip">{{ item.tip }}</view>
</view>
</scroll-view>
</template>
<script setup>
const props = defineProps({
days: {
type: Array,
required: true
},
modelValue: {
type: String,
required: true
}
});
const emit = defineEmits(['update:modelValue']);
</script>
<style scoped lang="scss">
.date-bar {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
background: #fff;
padding: 0 8rpx 0 8rpx;
width: 100%;
white-space: nowrap;
}
.date-item {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 68rpx;
min-width: 68rpx;
margin-right: 8rpx;
margin-top: 12rpx;
cursor: pointer;
height: 76rpx;
border-radius: 14rpx;
transition: background .18s;
}
.week {
font-size: 13px;
color: #b1b1b1;
margin-bottom: 4rpx;
}
.date {
font-size: 21px;
font-weight: 500;
color: #333;
}
.tips {
font-size: 10px;
color: #fa5e5e;
margin-top: 2rpx;
}
.selected {
background: linear-gradient(180deg, #eaf5ff 70%, #c0eaff 100%);
color: #2885ff;
border-bottom: 4rpx solid #2885ff;
}
.holiday .date {
color: #fa5e5e;
}
.weekend .week {
color: #179c29;
}
</style>

27
components/FabPlus.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<button class="fab-plus" @click="$emit('click')"></button>
</template>
<style scoped lang="scss">
.fab-plus {
position: fixed;
bottom: 100rpx;
right: 48rpx;
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: #2885ff;
color: #fff;
box-shadow: 0 6rpx 32rpx 0 #90c3fa;
font-size: 52rpx;
z-index: 9;
border: none;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow .3s;
}
.fab-plus:active {
box-shadow: 0 2rpx 12rpx 0 #b1d8fc;
background:#0D5ECD;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<view class="event-block" :style="{background: event.color||'#e3fae6'}">
<view class="event-time">{{ event.startHour }}:{{ event.startMin || '00' }}</view>
<view class="event-title">{{ event.title }}</view>
</view>
</template>
<script setup>
const props = defineProps({
event: {
type: Object,
required: true
}
});
</script>
<style scoped lang="scss">
.event-block {
padding: 8rpx 18rpx 8rpx 10rpx;
border-radius: 8rpx;
margin: 4rpx 0;
font-size: 13px;
min-width: 120rpx;
color: #3a3a3a;
display: flex;
align-items: center;
}
.event-time {
color: #fa5e5e;
font-size: 11px;
margin-right: 7rpx;
}
.event-title {
font-weight: 500;
}
</style>

59
components/TimeTable.vue Normal file
View File

@ -0,0 +1,59 @@
<template>
<view class="schedule-timeline">
<view
v-for="hour in hours"
:key="hour"
class="timeline-row"
>
<view class="time-label">{{ hour }}:00</view>
<view class="schedule-cell">
<ScheduleBlock
v-for="item in getEventsByHour(hour)"
:key="item.id"
:event="item"
/>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
import ScheduleBlock from './ScheduleBlock.vue';
const props = defineProps({
hours: {
type: Array,
default: () => Array.from({length: 12}, (_,i) => i+8) // 8~19
},
events: {
type: Array,
default: () => []
}
});
//
function getEventsByHour(hour) {
return props.events.filter(e => e.startHour === hour);
}
</script>
<style scoped lang="scss">
.schedule-timeline {
padding: 0 12rpx 60px;
}
.timeline-row {
display: flex;
border-bottom: 1px dashed #e3e3e3;
align-items: center;
height: 52px;
}
.time-label {
width: 44px;
color: #bbb;
font-size: 13px;
}
.schedule-cell {
flex: 1;
position: relative;
min-height: 44px;
}
</style>

60
components/TopTabs.vue Normal file
View File

@ -0,0 +1,60 @@
<template>
<view class="top-tabs">
<view
v-for="tab in tabs"
:key="tab.value"
class="tab"
:class="{active: tab.value === modelValue}"
@click="$emit('update:modelValue', tab.value)"
>
{{ tab.label }}
</view>
</view>
</template>
<script setup>
const props = defineProps({
tabs: {
type: Array,
required: true,
default: () => []
},
modelValue: {
type: [String, Number],
required: true
}
});
const emit = defineEmits(['update:modelValue']);
</script>
<style scoped lang="scss">
.top-tabs {
display: flex;
border-bottom: 1px solid #eee;
background: #fff;
}
.tab {
flex: 1;
padding: 24rpx 0 18rpx 0;
text-align: center;
font-size: 15px;
color: #888;
position: relative;
transition: color .2s;
}
.tab.active {
color: #2885ff;
font-weight: 600;
}
.tab.active::after {
content: '';
display: block;
position: absolute;
left: 25%;
right: 25%;
height: 4rpx;
border-radius: 3rpx;
background: #2885ff;
bottom: 0;
}
</style>

View File

@ -1,83 +1,106 @@
<template>
<uv-tabs :list="list" @click="click"></uv-tabs>
<view class="content">
<view class="text-area">
<text class="title">{{ title }}</text>
</view>
<uv-icon name="photo" size="30" color="#909399"></uv-icon>
<button class="uv-reset-button">点击登录</button>
<button>点击登录</button>
<view>
<uv-calendar ref="calendar" mode="single" @confirm="confirm"></uv-calendar>
<button @click="open">打开</button>
</view>
</view>
<uv-tabbar :value="value" @change="val => value = val">
<uv-tabbar-item text="首页" icon="home" dot></uv-tabbar-item>
<uv-tabbar-item text="放映厅" icon="photo" badge="3"></uv-tabbar-item>
<uv-tabbar-item text="直播" icon="play-right"></uv-tabbar-item>
<uv-tabbar-item text="我的" icon="account"></uv-tabbar-item>
</uv-tabbar>
<!-- 顶部Tabs栏 -->
<TopTabs :tabs="topTabs" v-model="topTabValue" />
<!-- 日期条 -->
<DateBar :days="weekDays" v-model="selectedDate" />
<!-- 时间轴表格 -->
<TimeTable :hours="hours" :events="eventsInDay" />
<!-- 悬浮新建按钮 -->
<FabPlus @click="showAdd = true" />
<!-- 新建日程弹窗 -->
<AddEventModal :show="showAdd" @ok="addEvent" @cancel="showAdd = false" />
<!-- 底部导航 -->
<BottomTabbar :items="tabbarItems" v-model="tabbarVal" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
const title = ref('Hello UNI');
const value = ref(0);
const list = [
{ name: '关注' },
{ name: '推荐' },
{ name: '电影' },
{ name: '科技' },
import { ref, computed } from 'vue';
import TopTabs from '@/components/TopTabs.vue';
import DateBar from '@/components/DateBar.vue';
import TimeTable from '@/components/TimeTable.vue';
import FabPlus from '@/components/FabPlus.vue';
import BottomTabbar from '@/components/BottomTabbar.vue';
import AddEventModal from '@/components/AddEventModal.vue';
// tabs
const topTabs = [
{ label: '日程编辑', value: 0 },
{ label: '内容看板', value: 1 },
{ label: '待办事项', value: 2 },
{ label: '消息内容', value: 3 }
];
const topTabValue = ref(0);
const calendar = ref(null);
const click = (item) => {
console.log('item', item);
};
const open = () => {
if (calendar.value) calendar.value.open();
};
const confirm = (e) => {
console.log('日历选择:', e);
};
onMounted(() => {
if (uni && uni.$uv && uni.$uv.os) {
console.log(uni.$uv.os());
//
function getWeekDays() {
const days = [];
const base = new Date();
base.setHours(0,0,0,0);
for(let i=-2; i<=4; i++) {
const d = new Date(base);
d.setDate(base.getDate() + i);
const ymd = d.toISOString().slice(0,10);
days.push({
date: ymd,
weekStr: '日一二三四五六'[d.getDay()],
day: d.getDate(),
tip: '',
isHoliday: d.getDay() === 0 || d.getDay() === 6,
isWeekend: d.getDay() === 6 || d.getDay() === 0,
});
}
});
return days;
}
const weekDays = ref(getWeekDays());
const selectedDate = ref(weekDays.value.find(i=>i.date === new Date().toISOString().slice(0,10))?.date || weekDays.value[2].date);
//
const hours = Array.from({length: 13}, (_,i)=>i+8); // 8~20
//
const allEvents = ref([
{id:1,title:'日程标题',startHour:15,startMin:30,color:'#e3fae6',date:weekDays.value[3].date},
{id:2,title:'日程标题',startHour:16,startMin:0,color:'#fae1e1',date:weekDays.value[3].date},
{id:3,title:'日程标题',startHour:23,startMin:0,color:'#e3fae6',date:weekDays.value[3].date},
]);
//
const eventsInDay = computed(() =>
allEvents.value.filter(e=>e.date===selectedDate.value)
);
// /
const showAdd = ref(false);
function addEvent(e) {
// e{title, startHour, startMin, color}date
allEvents.value.push({
...e, date: selectedDate.value,
id: Date.now()
});
showAdd.value = false;
}
//
const tabbarItems = [
{ label: '任务列表', value: 0, icon: 'list' },
{ label: '首页', value: 1, icon: 'home' },
{ label: '工作台', value: 2, icon: 'calendar' },
{ label: '月度考核', value: 3, icon: 'integral' },
{ label: '管理', value: 4, icon: 'account' }
];
const tabbarVal = ref(1);
</script>
<style lang="scss" scoped>
@import "@/uni.scss";
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: $uni-color-primary;
}
.icon {
width: 36rpx;
height: 36rpx;
:deep(.bottom-tabbar) { z-index: 1000 !important; }
.schedule-timeline {
padding-bottom: 130rpx !important;
}
</style>