yeg
2026-09-09 528d52f2c7c6f911c54cae6e3f25500eab4384fe
src/views/TodoList.vue
@@ -1,121 +1,86 @@
<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>
@@ -123,8 +88,8 @@
    </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>