<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { getCurrentUser } from '@/utils/auth'
|
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 }[]>([])
|
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} 条待办`)
|
}
|
|
function resetSearch() {
|
Object.assign(filters, { owner: '', status: '', dateRange: null })
|
appliedFilters.value = { owner: '', status: '', dateRange: null }
|
currentPage.value = 1
|
}
|
|
function openCreate() {
|
Object.assign(createForm, { content: '', owner: '' })
|
createDialogVisible.value = true
|
}
|
|
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
|
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 {
|
// 用户取消处理。
|
}
|
}
|
|
async function deleteTodo(item: TodoRecord) {
|
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 {
|
// 用户取消删除。
|
}
|
}
|
</script>
|
|
<template>
|
<div class="todo-page">
|
<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 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="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>
|
</tbody>
|
</table>
|
</div>
|
<footer class="pagination-bar"><span>显示 {{ pagedRecords.length }} 条,共 {{ filteredRecords.length }} 条</span><el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="filteredRecords.length" layout="sizes, prev, pager, next" background /></footer>
|
</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>
|
</el-dialog>
|
</div>
|
</template>
|
|
<style scoped>
|
.todo-page{width:100%}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.primary-button:hover{background:#176fcb;border-color:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:hover{color:var(--blue);border-color:#9dc8f5}.filter-panel,.result-panel{background:#fff;border:1px solid #d8e1e9}.filter-panel{margin-bottom:16px}.section-heading,.result-toolbar{display:flex;align-items:center;justify-content:space-between;background:var(--surface-soft);border-bottom:1px solid var(--line)}.section-heading{height:44px;padding:0 16px}.section-heading>div,.result-toolbar>div{display:flex;align-items:center;gap:8px}.section-heading>div>i,.result-toolbar>div:first-child>i{width:3px;height:16px;background:#2b78a3}.section-heading strong,.result-toolbar strong{color:var(--ink);font-size:15px}.section-heading span,.result-toolbar span{color:#81909d;font-size:10px}.section-heading small{color:#647380;font-size:11px}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px;padding:16px}.filter-grid label{width:100%;min-width:0;display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor){width:100%}.filter-grid :deep(.el-input__wrapper),.filter-grid :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.filter-actions{display:flex;justify-content:flex-end;gap:8px}.result-toolbar{height:50px;padding:0 16px}.table-wrap{overflow-x:auto}table{width:100%;min-width:1000px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}th,td{height:50px;padding:0 12px;border-right:1px solid #dde7f0;border-bottom:1px solid #dde7f0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th{height:40px;background:#eaf4ff;border-color:#c7e0ff;font-size:12px}th:nth-child(1){width:120px}th:nth-child(2){width:480px}th:nth-child(3){width:130px}th:nth-child(4){width:120px}th:last-child{width:180px}td strong{color:#26333f}.id-cell{color:#647380;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.status-tag{display:inline-flex;align-items:center;gap:6px;padding:4px 7px;border:1px solid;font-size:11px;line-height:1}.status-tag i{width:5px;height:5px;border-radius:50%;background:currentColor}.status-tag.pending{color:#b06a18;background:#fff8e9;border-color:#efd59e}.status-tag.handled{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.action-column{position:sticky;right:0;z-index:2;background:#fff;box-shadow:inset 1px 0 0 #b4c7d9,-8px 0 14px rgba(23,49,74,.12);text-align:center}.action-column{overflow:visible}th.action-column{z-index:3;background:#eaf4ff}.row-actions{display:flex;justify-content:center;gap:16px}.row-actions button{border:0;color:var(--blue);background:transparent;font-size:12px;cursor:pointer}.row-actions .delete-action{color:#ad4a4a}tbody tr:hover,tbody tr:hover td.action-column{background:#f7fbff}.empty-cell{height:160px;color:#8795a3;text-align:center}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;padding:8px 14px 8px 16px;border-top:1px solid var(--line)}.pagination-bar>span{color:#7b8794;font-size:11px}.create-form{display:grid;gap:18px}.create-form label{display:grid;grid-template-columns:78px minmax(0,1fr);align-items:start;gap:12px;color:#52606d;font-size:12px}.create-form label>span{padding-top:9px}.create-form :deep(.el-select){width:100%}.create-form :deep(.el-textarea__inner),.create-form :deep(.el-select__wrapper){border-radius:3px;background:#f8fafc}@media(max-width:1100px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.filter-grid{grid-template-columns:1fr}.filter-actions{justify-content:stretch}.filter-actions button{flex:1}.section-heading span,.result-toolbar span{display:none}.result-toolbar{height:auto;min-height:50px;gap:10px;padding-top:8px;padding-bottom:8px}.pagination-bar{align-items:flex-start;flex-direction:column;gap:10px}.create-form label{grid-template-columns:1fr}.create-form label>span{padding-top:0}}
|
</style>
|