| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { getCurrentUser } from '@/utils/auth' |
| | | import { getUserInfo } from '@/api/auth' |
| | | import { getTodoOwners, type TodoOwner } from '@/api/todos' |
| | | import { useTodos, type TodoRecord, type TodoStatus } from '@/stores/todos' |
| | | import { getEnabledUserOptions } from '@/api/users' |
| | | |
| | | interface FilterState { |
| | | owner: string |
| | | status: string |
| | | dateRange: [string, string] | null |
| | | } |
| | | |
| | | const owners = ref<{ id:number; name:string }[]>([]) |
| | | interface FilterState { owner: number | ''; status: string; dateRange: [string, string] | null } |
| | | const owners = ref<TodoOwner[]>([]) |
| | | const statuses: TodoStatus[] = ['待处理', '已处理'] |
| | | const { records, createTodo, handleTodo, removeTodo, load } = useTodos() |
| | | const currentOwner = computed(() => getCurrentUser()?.displayName || getCurrentUser()?.username || 'Admin') |
| | | const filters = reactive<FilterState>({ owner: '', status: '', dateRange: null }) |
| | | const appliedFilters = ref<FilterState>({ owner: '', status: '', dateRange: null }) |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | const createDialogVisible = ref(false) |
| | | const createForm = reactive({ content: '', owner: '' }) |
| | | onMounted(async () => { await load(); owners.value = await getEnabledUserOptions() }) |
| | | |
| | | const filteredRecords = computed(() => { |
| | | const query = appliedFilters.value |
| | | return records.value |
| | | .filter(item => item.owner === currentOwner.value) |
| | | .filter(item => !query.owner || query.owner === '全部' || item.owner === query.owner) |
| | | .filter(item => !query.status || query.status === '全部' || item.status === query.status) |
| | | .filter(item => !query.dateRange || (item.operatedAt.slice(0, 10) >= query.dateRange[0] && item.operatedAt.slice(0, 10) <= query.dateRange[1])) |
| | | .sort((a, b) => b.operatedAt.localeCompare(a.operatedAt)) |
| | | }) |
| | | |
| | | const pagedRecords = computed(() => { |
| | | const start = (currentPage.value - 1) * pageSize.value |
| | | return filteredRecords.value.slice(start, start + pageSize.value) |
| | | }) |
| | | |
| | | function applySearch() { |
| | | appliedFilters.value = { ...filters, dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null } |
| | | currentPage.value = 1 |
| | | ElMessage.success(`已找到 ${filteredRecords.value.length} 条待办`) |
| | | const currentPage = ref(1); const pageSize = ref(10) |
| | | const createDialogVisible = ref(false); const createForm = reactive({ content: '', ownerId: null as number | null }) |
| | | const permissions = ref<string[]>([]); const busy = ref(false); const loading = ref(false); const loadError = ref('') |
| | | const has = (action: string) => permissions.value.includes('*:*:*') || permissions.value.includes('todo:list:' + action) |
| | | const filterOwners = computed(() => [...new Map(records.value.map(item => [item.ownerId, { id: item.ownerId, name: item.owner }])).values()]) |
| | | async function loadPage() { |
| | | loading.value = true; loadError.value = '' |
| | | try { permissions.value = (await getUserInfo()).permissions || []; await load() } |
| | | catch (error) { loadError.value = error instanceof Error ? error.message : '待办加载失败' } |
| | | finally { loading.value = false } |
| | | } |
| | | |
| | | function resetSearch() { |
| | | Object.assign(filters, { owner: '', status: '', dateRange: null }) |
| | | appliedFilters.value = { owner: '', status: '', dateRange: null } |
| | | currentPage.value = 1 |
| | | onMounted(loadPage) |
| | | const filteredRecords = computed(() => records.value.filter(item => !appliedFilters.value.owner || item.ownerId === appliedFilters.value.owner) |
| | | .filter(item => !appliedFilters.value.status || item.status === appliedFilters.value.status) |
| | | .filter(item => !appliedFilters.value.dateRange || (item.operatedAt.slice(0, 10) >= appliedFilters.value.dateRange[0] && item.operatedAt.slice(0, 10) <= appliedFilters.value.dateRange[1])) |
| | | .slice().sort((a, b) => b.operatedAt.localeCompare(a.operatedAt) || b.id - a.id)) |
| | | const pagedRecords = computed(() => filteredRecords.value.slice((currentPage.value - 1) * pageSize.value, currentPage.value * pageSize.value)) |
| | | function applySearch() { appliedFilters.value = { ...filters, dateRange: filters.dateRange ? [...filters.dateRange] : null }; currentPage.value = 1 } |
| | | function resetSearch() { Object.assign(filters, { owner: '', status: '', dateRange: null }); appliedFilters.value = { ...filters }; currentPage.value = 1 } |
| | | async function openCreate() { |
| | | try { owners.value = await getTodoOwners(); Object.assign(createForm, { content: '', ownerId: null }); createDialogVisible.value = true } |
| | | catch (error) { ElMessage.error(error instanceof Error ? error.message : '负责人加载失败') } |
| | | } |
| | | |
| | | function openCreate() { |
| | | Object.assign(createForm, { content: '', owner: '' }) |
| | | createDialogVisible.value = true |
| | | async function submitCreate() { |
| | | if (busy.value) return |
| | | if (!createForm.content.trim() || !createForm.ownerId) { ElMessage.warning('请填写内容并选择负责人'); return } |
| | | busy.value = true |
| | | try { await createTodo({ content: createForm.content.trim(), ownerId: createForm.ownerId }); createDialogVisible.value = false; currentPage.value = 1; ElMessage.success('待办已新增') } |
| | | catch (error) { ElMessage.error(error instanceof Error ? error.message : '新增失败') } |
| | | finally { busy.value = false } |
| | | } |
| | | |
| | | function submitCreate() { |
| | | const content = createForm.content.trim() |
| | | if (!content) { ElMessage.warning('请输入待办内容'); return } |
| | | if (!createForm.owner) { ElMessage.warning('请选择负责人'); return } |
| | | const owner = owners.value.find(item => item.name === createForm.owner) |
| | | if (!owner) { ElMessage.warning('负责人不存在'); return } |
| | | createTodo({ content, ownerId: owner.id, owner: owner.name }) |
| | | createDialogVisible.value = false |
| | | currentPage.value = 1 |
| | | ElMessage.success('待办已新增') |
| | | } |
| | | |
| | | async function processTodo(item: TodoRecord) { |
| | | if (item.status === '已处理' || item.owner !== currentOwner.value) return |
| | | if (busy.value || item.status === '已处理') return |
| | | try { |
| | | const result = await ElMessageBox.prompt(`请填写“${item.content}”的处理说明。`, '处理待办', { |
| | | confirmButtonText: '完成处理', cancelButtonText: '取消', inputType: 'textarea', inputPlaceholder: '请输入处理说明', |
| | | inputValidator: value => value.trim() ? true : '请输入处理说明', |
| | | }) |
| | | handleTodo(item.id, result.value.trim()) |
| | | ElMessage.success('待办已处理') |
| | | } catch { |
| | | // 用户取消处理。 |
| | | } |
| | | const { value } = await ElMessageBox.prompt('请填写“' + item.content + '”的处理说明。', '处理待办', { confirmButtonText: '完成处理', cancelButtonText: '取消', inputType: 'textarea', inputValidator: value => Boolean(value?.trim()) && value.trim().length <= 500 || '请输入不超过500字的处理说明' }) |
| | | busy.value = true; await handleTodo(item.id, value.trim()); ElMessage.success('待办已处理') |
| | | } catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '处理失败') } |
| | | finally { busy.value = false } |
| | | } |
| | | |
| | | async function deleteTodo(item: TodoRecord) { |
| | | if (busy.value) return |
| | | try { |
| | | await ElMessageBox.confirm(`删除待办“${item.content}”后无法恢复,是否继续?`, '确认删除待办', { |
| | | confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm', |
| | | }) |
| | | if (item.owner !== currentOwner.value) return |
| | | removeTodo(item.id) |
| | | currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))) |
| | | ElMessage.success('待办已删除') |
| | | } catch { |
| | | // 用户取消删除。 |
| | | } |
| | | await ElMessageBox.confirm('删除待办“' + item.content + '”后无法恢复,是否继续?', '确认删除待办', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }) |
| | | busy.value = true; await removeTodo(item.id); currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value))); ElMessage.success('待办已删除') |
| | | } catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '删除失败') } |
| | | finally { busy.value = false } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="todo-page"> |
| | | <el-alert v-if="loadError" :title="loadError" type="error" :closable="false" /><el-button v-if="loadError" @click="loadPage">重试</el-button> |
| | | <section class="filter-panel" aria-label="待办查询条件"> |
| | | <header class="section-heading"><div><i></i><strong>查询条件</strong><span>TODO FILTER CONDITIONS</span></div><small>共 {{ records.length }} 条待办</small></header> |
| | | <div class="filter-grid"> |
| | | <label><span>负责人</span><el-select v-model="filters.owner" clearable placeholder="全部"><el-option label="全部" value="全部" /><el-option v-for="item in owners" :key="item.id" :label="item.name" :value="item.name" /></el-select></label> |
| | | <label><span>状态</span><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="全部" value="全部" /><el-option v-for="item in statuses" :key="item" :label="item" :value="item" /></el-select></label> |
| | | <label><span>负责人</span><el-select v-model="filters.owner" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in filterOwners" :key="item.id" :label="item.name" :value="item.id" /></el-select></label> |
| | | <label><span>状态</span><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in statuses" :key="item" :label="item" :value="item" /></el-select></label> |
| | | <label class="date-filter"><span>操作时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" unlink-panels /></label> |
| | | <div class="filter-actions"><button class="primary-button" type="button" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="result-panel"> |
| | | <div class="result-toolbar"><div><i></i><strong>待办列表</strong><span>{{ filteredRecords.length }} RESULTS · 按操作时间倒序</span></div><button class="primary-button" type="button" @click="openCreate"><el-icon><Plus /></el-icon>新增待办</button></div> |
| | | <div class="result-toolbar"><div><i></i><strong>待办列表</strong><span>{{ filteredRecords.length }} RESULTS · 按操作时间倒序</span></div><button v-if="has('add')" class="primary-button" type="button" @click="openCreate"><el-icon><Plus /></el-icon>新增待办</button></div> |
| | | <div class="table-wrap"> |
| | | <table> |
| | | <thead><tr><th>待办ID</th><th>待办内容</th><th>负责人</th><th>状态</th><th class="action-column">操作</th></tr></thead> |
| | | <tbody> |
| | | <tr v-for="item in pagedRecords" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td :title="item.content"><strong>{{ item.content }}</strong></td><td>{{ item.owner }}</td><td><span class="status-tag" :class="item.status === '待处理' ? 'pending' : 'handled'"><i></i>{{ item.status }}</span></td><td class="action-column"><div class="row-actions"><button v-if="item.status === '待处理'" type="button" @click="processTodo(item)">处理待办</button><button class="delete-action" type="button" @click="deleteTodo(item)">删除</button></div></td></tr> |
| | | <tr v-if="!pagedRecords.length"><td colspan="5" class="empty-cell">暂无符合条件的待办</td></tr> |
| | | <tr v-for="item in pagedRecords" :key="item.id"><td class="id-cell">#{{ item.id }}</td><td :title="item.content"><strong>{{ item.content }}</strong></td><td>{{ item.owner }}</td><td><span class="status-tag" :class="item.status === '待处理' ? 'pending' : 'handled'"><i></i>{{ item.status }}</span></td><td class="action-column"><div class="row-actions"><button v-if="has('handle') && item.status === '待处理'" :disabled="busy" type="button" @click="processTodo(item)">处理待办</button><button v-if="has('remove')" :disabled="busy" class="delete-action" type="button" @click="deleteTodo(item)">删除</button></div></td></tr> |
| | | <tr v-if="!pagedRecords.length"><td colspan="5" class="empty-cell">{{ loading ? '正在加载待办' : loadError ? '待办加载失败' : '暂无符合条件的待办' }}</td></tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | |
| | | </section> |
| | | |
| | | <el-dialog v-model="createDialogVisible" title="新增待办" width="600px" align-center destroy-on-close> |
| | | <div class="create-form"><label><span>待办内容</span><el-input v-model="createForm.content" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="请输入待办内容" /></label><label><span>负责人</span><el-select v-model="createForm.owner" filterable placeholder="请选择负责人"><el-option v-for="item in owners" :key="item.id" :label="item.name" :value="item.name" /></el-select></label></div> |
| | | <template #footer><button class="secondary-button" type="button" @click="createDialogVisible = false">取消</button><button class="primary-button" type="button" @click="submitCreate"><el-icon><Check /></el-icon>确定新增</button></template> |
| | | <div class="create-form"><label><span>待办内容</span><el-input v-model="createForm.content" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="请输入待办内容" /></label><label><span>负责人</span><el-select v-model="createForm.ownerId" filterable placeholder="请选择负责人"><el-option v-for="item in owners" :key="item.id" :label="item.name" :value="item.id" /></el-select></label></div> |
| | | <template #footer><button class="secondary-button" type="button" @click="createDialogVisible = false">取消</button><button class="primary-button" type="button" :disabled="busy" @click="submitCreate"><el-icon><Check /></el-icon>确定新增</button></template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |