feat(frontend): settings window, i18n, admin UI, books, tasks, calendar, timer
- Settings window with tabs: profile (name, email, avatar, password), appearance (light/dark/system), language (de/en/zh), background (gradient presets, custom URL, blur toggle); persisted server-side - i18n dictionaries (de/en/zh) applied across nav, sidebar, tasks, calendar, time tracking, admin, invites and comments - Greeting header with time-of-day message, live clock and date; cooler theme, cards, kanban, calendar grid and data-table styling - Shared sidebar: nav, collapsible books with per-book pages, top-level pages, search; user chip opens settings - Tasks kanban with priorities, overdue flags and book/page links; calendar month view fed by task deadlines; time view with entries, durations and totals; top-right live timer with optional book link - User discovery autocomplete in the invite card; editor gains tables and checklists; standalone theme buttons removed
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } 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';
|
||||
|
||||
const ws = useWorkspaceStore();
|
||||
const L = useLocaleStore();
|
||||
const router = useRouter();
|
||||
|
||||
const tasks = ref([]);
|
||||
const title = ref('');
|
||||
const due = ref('');
|
||||
const priority = ref('normal');
|
||||
const bookId = ref('');
|
||||
const pageId = ref('');
|
||||
|
||||
const cols = computed(() => ({
|
||||
todo: tasks.value.filter((t) => t.status === 'todo'),
|
||||
doing: tasks.value.filter((t) => t.status === 'doing'),
|
||||
done: tasks.value.filter((t) => t.status === 'done'),
|
||||
}));
|
||||
|
||||
function isOverdue(t) {
|
||||
return t.status !== 'done' && t.due_at && new Date(t.due_at) < new Date();
|
||||
}
|
||||
function fmtDate(d) {
|
||||
return d ? new Date(d).toLocaleString() : L.t('task.noDue');
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!ws.currentId) return;
|
||||
const { data } = await api.get(`/tasks/by-workspace/${ws.currentId}`);
|
||||
tasks.value = data.tasks;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
if (!title.value.trim()) return;
|
||||
const { data } = await api.post('/tasks', {
|
||||
workspace_id: ws.currentId,
|
||||
title: title.value.trim(),
|
||||
priority: priority.value,
|
||||
due_at: due.value || null,
|
||||
book_id: bookId.value || null,
|
||||
page_id: pageId.value || null,
|
||||
});
|
||||
tasks.value.unshift(data.task);
|
||||
title.value = '';
|
||||
due.value = '';
|
||||
bookId.value = '';
|
||||
pageId.value = '';
|
||||
}
|
||||
|
||||
async function setStatus(t, status) {
|
||||
const { data } = await api.patch(`/tasks/${t.id}`, { status });
|
||||
Object.assign(t, data.task);
|
||||
}
|
||||
|
||||
async function remove(t) {
|
||||
if (!confirm('🗑️?')) return;
|
||||
await api.delete(`/tasks/${t.id}`);
|
||||
tasks.value = tasks.value.filter((x) => x.id !== t.id);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!ws.workspaces.length) await ws.fetchWorkspaces();
|
||||
await ws.fetchBooks();
|
||||
await ws.fetchPages();
|
||||
await load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<Sidebar />
|
||||
<main class="main">
|
||||
<h2>✅ {{ L.t('task.title') }}</h2>
|
||||
<div class="card new-task">
|
||||
<div class="row wrap">
|
||||
<input v-model="title" :placeholder="L.t('task.ph')" @keyup.enter="create" style="flex: 2" />
|
||||
<input v-model="due" type="datetime-local" :title="L.t('task.due')" />
|
||||
<select v-model="priority" style="max-width: 130px">
|
||||
<option value="low">🟢 {{ L.t('task.low') }}</option>
|
||||
<option value="normal">🟡 {{ L.t('task.normal') }}</option>
|
||||
<option value="high">🔴 {{ L.t('task.high') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row wrap">
|
||||
<select v-model="bookId">
|
||||
<option value="">{{ L.t('task.book') }}: {{ L.t('task.none') }}</option>
|
||||
<option v-for="b in ws.books" :key="b.id" :value="b.id">{{ b.icon }} {{ b.title }}</option>
|
||||
</select>
|
||||
<select v-model="pageId">
|
||||
<option value="">{{ L.t('task.page') }}: {{ L.t('task.none') }}</option>
|
||||
<option v-for="p in ws.pages" :key="p.id" :value="p.id">{{ p.title }}</option>
|
||||
</select>
|
||||
<button @click="create">+ {{ L.t('task.new') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban">
|
||||
<div v-for="col in ['todo', 'doing', 'done']" :key="col" class="kanban-col">
|
||||
<h3>{{ L.t(`task.${col}`) }} ({{ cols[col].length }})</h3>
|
||||
<div v-for="t in cols[col]" :key="t.id" class="card task-card" :class="{ overdue: isOverdue(t) }">
|
||||
<strong>{{ t.title }}</strong>
|
||||
<div class="muted">
|
||||
{{ t.priority === 'high' ? '🔴' : t.priority === 'low' ? '🟢' : '🟡' }}
|
||||
🕒 {{ fmtDate(t.due_at) }}
|
||||
<span v-if="isOverdue(t)" class="overdue-tag">{{ L.t('task.overdue') }}</span>
|
||||
</div>
|
||||
<div class="muted">
|
||||
<span v-if="t.book_title">📚 {{ t.book_title }}</span>
|
||||
<span v-if="t.page_title" class="link" @click="router.push(`/page/${t.page_id}`)">📄 {{ t.page_title }}</span>
|
||||
</div>
|
||||
<div class="row wrap">
|
||||
<button v-if="t.status !== 'todo'" class="ghost mini" @click="setStatus(t, 'todo')">{{ L.t('task.toTodo') }}</button>
|
||||
<button v-if="t.status === 'todo'" class="ghost mini" @click="setStatus(t, 'doing')">{{ L.t('task.toDoing') }}</button>
|
||||
<button v-if="t.status === 'doing'" class="ghost mini" @click="setStatus(t, 'done')">{{ L.t('task.toDone') }}</button>
|
||||
<button v-if="t.status === 'done'" class="ghost mini" @click="setStatus(t, 'todo')">{{ L.t('task.reopen') }}</button>
|
||||
<button class="ghost mini" @click="remove(t)">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user