- Chat rework: Chats (private DMs with user search, online dots, unread badges) and Team chats tabs; shared message component with reactions, edit, image attachments, message search and typing indicators - Flat modern inline SVG icon set (no external dependency) replaces emoji chrome across nav, headers, buttons, tabs and settings - Uploaded avatar/background images apply instantly (auto-save) with confirmation note; per-user permission matrix in dashboard members - Login localized, invite header uses icon, comment header cleaned
108 lines
3.5 KiB
Vue
108 lines
3.5 KiB
Vue
<script setup>
|
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import api from '../api';
|
|
import { useWorkspaceStore } from '../stores/workspace';
|
|
import { useLocaleStore } from '../stores/locale';
|
|
import Sidebar from '../components/Sidebar.vue';
|
|
import Icon from '../components/Icon.vue';
|
|
|
|
const ws = useWorkspaceStore();
|
|
const L = useLocaleStore();
|
|
const router = useRouter();
|
|
|
|
const cursor = ref(new Date());
|
|
cursor.value.setDate(1);
|
|
const tasks = ref([]);
|
|
|
|
const year = computed(() => cursor.value.getFullYear());
|
|
const month = computed(() => cursor.value.getMonth());
|
|
const monthName = computed(() =>
|
|
cursor.value.toLocaleDateString(L.locale === 'zh' ? 'zh-CN' : L.locale === 'de' ? 'de-DE' : 'en-US', {
|
|
month: 'long', year: 'numeric',
|
|
}),
|
|
);
|
|
|
|
const cells = computed(() => {
|
|
const first = new Date(year.value, month.value, 1);
|
|
const startDay = (first.getDay() + 6) % 7; // Monday first
|
|
const days = new Date(year.value, month.value + 1, 0).getDate();
|
|
const arr = [];
|
|
for (let i = 0; i < startDay; i++) arr.push(null);
|
|
for (let d = 1; d <= days; d++) arr.push(new Date(year.value, month.value, d));
|
|
return arr;
|
|
});
|
|
|
|
function dayKey(d) {
|
|
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
}
|
|
const byDay = computed(() => {
|
|
const map = {};
|
|
for (const t of tasks.value) {
|
|
if (!t.due_at) continue;
|
|
const d = new Date(t.due_at);
|
|
const k = dayKey(d);
|
|
if (!map[k]) map[k] = [];
|
|
map[k].push(t);
|
|
}
|
|
return map;
|
|
});
|
|
|
|
function isToday(d) {
|
|
const n = new Date();
|
|
return d && d.toDateString() === n.toDateString();
|
|
}
|
|
|
|
async function load() {
|
|
if (!ws.currentId) return;
|
|
const from = new Date(year.value, month.value, 1).toISOString();
|
|
const to = new Date(year.value, month.value + 1, 0, 23, 59).toISOString();
|
|
const { data } = await api.get(`/tasks/by-workspace/${ws.currentId}`, { params: { from, to } });
|
|
tasks.value = data.tasks;
|
|
}
|
|
|
|
function prev() { cursor.value = new Date(year.value, month.value - 1, 1); }
|
|
function next() { cursor.value = new Date(year.value, month.value + 1, 1); }
|
|
function goToday() { const n = new Date(); cursor.value = new Date(n.getFullYear(), n.getMonth(), 1); }
|
|
|
|
watch(cursor, load);
|
|
|
|
onMounted(async () => {
|
|
if (!ws.workspaces.length) await ws.fetchWorkspaces();
|
|
await load();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="layout">
|
|
<Sidebar />
|
|
<main class="main">
|
|
<div class="topbar">
|
|
<h2 style="margin: 0"><Icon name="calendar" :size="22" /> {{ L.t('cal.title') }} — {{ monthName }}</h2>
|
|
<div class="row">
|
|
<button class="ghost" @click="prev">◀</button>
|
|
<button class="ghost" @click="goToday">{{ L.t('cal.today') }}</button>
|
|
<button class="ghost" @click="next">▶</button>
|
|
</div>
|
|
</div>
|
|
<div class="calendar">
|
|
<div class="cal-head" v-for="d in ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']" :key="d">{{ d }}</div>
|
|
<div v-for="(d, i) in cells" :key="i" class="cal-cell" :class="{ today: isToday(d) }">
|
|
<template v-if="d">
|
|
<div class="cal-day">{{ d.getDate() }}</div>
|
|
<div
|
|
v-for="t in (byDay[dayKey(d)] || [])" :key="t.id"
|
|
class="cal-task" :class="{ done: t.status === 'done' }"
|
|
:title="t.title"
|
|
@click="router.push('/tasks')"
|
|
>
|
|
{{ t.status === 'done' ? '✅' : '▸' }} {{ t.title }}
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<p class="muted">▸ {{ L.t('task.due') }} → {{ L.t('cal.title') }} ✨</p>
|
|
</main>
|
|
</div>
|
|
</template>
|