|
@@ -0,0 +1,623 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="flow-page">
|
|
|
|
|
+ <!-- 通知列表 -->
|
|
|
|
|
+ <FlowTaskCardList
|
|
|
|
|
+ v-model:selected-keys="selectedTaskKeys"
|
|
|
|
|
+ v-model:search-value="queryParams.title"
|
|
|
|
|
+ title="我的通知"
|
|
|
|
|
+ :items="dataSource"
|
|
|
|
|
+ :loading="loading"
|
|
|
|
|
+ :pagination="pagination"
|
|
|
|
|
+ row-key="id"
|
|
|
|
|
+ search-placeholder="通过名称搜索"
|
|
|
|
|
+ empty-text="暂无通知"
|
|
|
|
|
+ @search="handleSearch"
|
|
|
|
|
+ @refresh="loadData"
|
|
|
|
|
+ @row-click="openDrawer"
|
|
|
|
|
+ @update:page="pagination.onChange"
|
|
|
|
|
+ @update:page-size="pagination.onUpdatePageSize"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #filters>
|
|
|
|
|
+ <n-select
|
|
|
|
|
+ v-model:value="queryParams.isRead"
|
|
|
|
|
+ placeholder="已读状态"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ class="category-select"
|
|
|
|
|
+ :options="readStatusOptions"
|
|
|
|
|
+ @update:value="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ <NButton secondary @click="handleReset">
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </NButton>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #batch-actions>
|
|
|
|
|
+ <NButton
|
|
|
|
|
+ v-if="selectedTaskKeys.length > 0"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ @click="handleMarkSelectedAsRead"
|
|
|
|
|
+ >
|
|
|
|
|
+ 标记已读
|
|
|
|
|
+ </NButton>
|
|
|
|
|
+ <NButton
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ type="info"
|
|
|
|
|
+ @click="handleMarkAllAsRead"
|
|
|
|
|
+ >
|
|
|
|
|
+ 全部已读
|
|
|
|
|
+ </NButton>
|
|
|
|
|
+ <span v-if="unreadCount > 0" class="task-list-hint unread">
|
|
|
|
|
+ <i class="i-material-symbols:notifications-active" />
|
|
|
|
|
+ {{ unreadCount }} 条未读
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #status="{ row }">
|
|
|
|
|
+ <span class="task-status-pill" :class="row.isRead === 1 ? 'notification-read' : 'notification-unread'">
|
|
|
|
|
+ {{ row.isRead === 1 ? '已读' : '未读' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #title="{ row }">
|
|
|
|
|
+ <div class="notification-title-row">
|
|
|
|
|
+ <span v-if="row.isRead !== 1" class="unread-dot" />
|
|
|
|
|
+ {{ getRowDisplayTitle(row) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #meta="{ row }">
|
|
|
|
|
+ <span><span class="task-meta-label">申请人</span> <span class="task-meta-value">{{ row.startUserName || '-' }}</span></span>
|
|
|
|
|
+ <span><span class="task-meta-label">提交时间</span> <span class="task-meta-value">{{ row.createTime || '-' }}</span></span>
|
|
|
|
|
+ <span><span class="task-meta-label">当前节点</span> <span class="task-meta-value">{{ getTaskDisplayName(row) }}</span></span>
|
|
|
|
|
+ <span><span class="task-meta-label">流程分类</span> <span class="task-meta-value">{{ getCategoryDisplayName(row) }}</span></span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #actions="{ row }">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-if="row.isRead !== 1"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="task-row-link-action info"
|
|
|
|
|
+ aria-label="标记已读"
|
|
|
|
|
+ @click.stop="handleMarkAsRead(row)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 标记已读
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button type="button" class="task-row-link-action primary" aria-label="查看详情" @click="openDrawer(row)">
|
|
|
|
|
+ <span>查看</span>
|
|
|
|
|
+ <i class="i-material-symbols:chevron-right" />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </FlowTaskCardList>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 通知详情弹窗 -->
|
|
|
|
|
+ <FlowTaskDetailShell
|
|
|
|
|
+ v-model:show="showDrawer"
|
|
|
|
|
+ :busy="loading"
|
|
|
|
|
+ :title="currentTask ? getRowDisplayTitle(currentTask) : '通知详情'"
|
|
|
|
|
+ :subtitle="getTaskDisplayName(currentTask, '') ? `当前节点:${getTaskDisplayName(currentTask)}` : ''"
|
|
|
|
|
+ :status-text="currentTask?.isRead === 1 ? '已读' : '未读'"
|
|
|
|
|
+ :status-class="currentTask?.isRead === 1 ? 'notification-read' : 'notification-unread'"
|
|
|
|
|
+ :status-icon="currentTask?.isRead === 1 ? 'i-material-symbols:check-circle' : 'i-material-symbols:notifications-active'"
|
|
|
|
|
+ :records="approvalHistory"
|
|
|
|
|
+ record-title="流转记录"
|
|
|
|
|
+ fullscreen
|
|
|
|
|
+ >
|
|
|
|
|
+ <template v-if="currentTask">
|
|
|
|
|
+ <section class="approval-detail-section">
|
|
|
|
|
+ <div class="approval-section-header">
|
|
|
|
|
+ <i class="i-material-symbols:info-outline" />
|
|
|
|
|
+ 基本信息
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field-grid">
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">当前节点</span>
|
|
|
|
|
+ <span class="approval-value">{{ getTaskDisplayName(currentTask) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">流程名称</span>
|
|
|
|
|
+ <span class="approval-value">{{ getProcessDisplayName(currentTask) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">流程分类</span>
|
|
|
|
|
+ <span class="approval-value">{{ getCategoryDisplayName(currentTask) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">发起人</span>
|
|
|
|
|
+ <span class="approval-value approval-user-inline">
|
|
|
|
|
+ <UserAvatar :name="currentTask.startUserName || '未知'" :size="24" />
|
|
|
|
|
+ {{ currentTask.startUserName || '-' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">发起部门</span>
|
|
|
|
|
+ <span class="approval-value">{{ currentTask.startDeptName || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">发起时间</span>
|
|
|
|
|
+ <span class="approval-value">{{ currentTask.createTime || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">已读状态</span>
|
|
|
|
|
+ <span class="approval-value">
|
|
|
|
|
+ <span class="task-status-pill" :class="currentTask.isRead === 1 ? 'notification-read' : 'notification-unread'">
|
|
|
|
|
+ {{ currentTask.isRead === 1 ? '已读' : '未读' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="approval-detail-section">
|
|
|
|
|
+ <n-collapse arrow-placement="right">
|
|
|
|
|
+ <n-collapse-item title="查看流程图" name="diagram">
|
|
|
|
|
+ <div class="approval-diagram">
|
|
|
|
|
+ <DingFlowViewer v-if="currentTask.processInstanceId" :process-instance-id="currentTask.processInstanceId" :compact="true" />
|
|
|
|
|
+ <NEmpty v-else description="暂无流程图" size="small" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </n-collapse-item>
|
|
|
|
|
+ </n-collapse>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section v-if="orderDetail" class="approval-detail-section">
|
|
|
|
|
+ <div class="approval-section-header">
|
|
|
|
|
+ <i class="i-material-symbols:receipt-long" />
|
|
|
|
|
+ 订单明细
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field-grid">
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">订单号</span>
|
|
|
|
|
+ <span class="approval-value">{{ orderDetail.orderNo || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">包房</span>
|
|
|
|
|
+ <span class="approval-value">{{ orderDetail.roomName || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">结账方式</span>
|
|
|
|
|
+ <span class="approval-value">{{ getSettleTypeLabel(orderDetail.settleType) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="approval-field">
|
|
|
|
|
+ <span class="approval-label">下单人</span>
|
|
|
|
|
+ <span class="approval-value">{{ orderDetail.createName || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 菜品明细表 -->
|
|
|
|
|
+ <div v-if="orderDetail.detailList && orderDetail.detailList.length" class="dish-table-wrap">
|
|
|
|
|
+ <table class="dish-table">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>菜品名称</th>
|
|
|
|
|
+ <th>分类</th>
|
|
|
|
|
+ <th>单价</th>
|
|
|
|
|
+ <th>数量</th>
|
|
|
|
|
+ <th>单位</th>
|
|
|
|
|
+ <th>小计</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ <tr v-for="item in orderDetail.detailList" :key="item.id">
|
|
|
|
|
+ <td>{{ item.dishName || '-' }}</td>
|
|
|
|
|
+ <td>{{ item.typeName || '-' }}</td>
|
|
|
|
|
+ <td>{{ formatMoney(item.salePrice) }}</td>
|
|
|
|
|
+ <td>{{ item.quantity }}</td>
|
|
|
|
|
+ <td>{{ item.unit || '-' }}</td>
|
|
|
|
|
+ <td>{{ formatMoney(item.amount) }}</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <NEmpty v-else description="暂无菜品明细" size="small" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 金额汇总 -->
|
|
|
|
|
+ <div class="amount-summary">
|
|
|
|
|
+ <div class="amount-row">
|
|
|
|
|
+ <span class="amount-label">订单总额</span>
|
|
|
|
|
+ <span class="amount-value">{{ formatMoney(orderDetail.totalAmount) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="orderDetail.settleType === 'DISCOUNT'" class="amount-row discount">
|
|
|
|
|
+ <span class="amount-label">折扣比例</span>
|
|
|
|
|
+ <span class="amount-value">{{ orderDetail.discountRate ? `${orderDetail.discountRate / 10} 折` : '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="orderDetail.discountAmount && orderDetail.discountAmount > 0" class="amount-row discount">
|
|
|
|
|
+ <span class="amount-label">优惠金额</span>
|
|
|
|
|
+ <span class="amount-value text-error">-{{ formatMoney(orderDetail.discountAmount) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="amount-row total">
|
|
|
|
|
+ <span class="amount-label">实收金额</span>
|
|
|
|
|
+ <span class="amount-value">{{ formatMoney(orderDetail.payableAmount) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section v-if="currentTask.isRead !== 1" class="approval-detail-section">
|
|
|
|
|
+ <div class="notification-action-bar">
|
|
|
|
|
+ <NButton type="primary" @click="handleMarkAsRead(currentTask)">
|
|
|
|
|
+ <i class="i-material-symbols:check-circle mr-2" />
|
|
|
|
|
+ 标记为已读
|
|
|
|
|
+ </NButton>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </FlowTaskDetailShell>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { NButton, NEmpty } from 'naive-ui'
|
|
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
+import flowApi from '@/api/flow'
|
|
|
|
|
+import UserAvatar from '@/components/common/UserAvatar.vue'
|
|
|
|
|
+import DingFlowViewer from '@/components/flow-designer/viewer/DingFlowViewer.vue'
|
|
|
|
|
+import FlowTaskCardList from '@/components/flow/FlowTaskCardList.vue'
|
|
|
|
|
+import FlowTaskDetailShell from '@/components/flow/FlowTaskDetailShell.vue'
|
|
|
|
|
+import { useUserStore } from '@/store'
|
|
|
|
|
+import { request } from '@/utils'
|
|
|
|
|
+import { buildFlowCategoryTreeOptions, resolveFlowCategoryLabel } from './utils/categoryOptions'
|
|
|
|
|
+import { getRowDisplayTitle, getTaskDisplayName } from './utils/processDisplay'
|
|
|
|
|
+
|
|
|
|
|
+const userStore = useUserStore()
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const dataSource = ref([])
|
|
|
|
|
+const pagination = reactive({
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ itemCount: 0,
|
|
|
|
|
+ showSizePicker: true,
|
|
|
|
|
+ pageSizes: [10, 20, 50],
|
|
|
|
|
+ onChange: (page) => {
|
|
|
|
|
+ pagination.page = page
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ },
|
|
|
|
|
+ onUpdatePageSize: (size) => {
|
|
|
|
|
+ pagination.pageSize = size
|
|
|
|
|
+ pagination.page = 1
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ },
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const queryParams = reactive({ title: '', isRead: null })
|
|
|
|
|
+const categoryTreeOptions = ref([])
|
|
|
|
|
+const unreadCount = ref(0)
|
|
|
|
|
+const selectedTaskKeys = ref([])
|
|
|
|
|
+
|
|
|
|
|
+// 抽屉状态
|
|
|
|
|
+const showDrawer = ref(false)
|
|
|
|
|
+const currentTask = ref(null)
|
|
|
|
|
+const approvalHistory = ref([])
|
|
|
|
|
+const orderDetail = ref(null)
|
|
|
|
|
+const orderLoading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const readStatusOptions = computed(() => [
|
|
|
|
|
+ { label: '未读', value: 0 },
|
|
|
|
|
+ { label: '已读', value: 1 },
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+function getCategoryDisplayName(row) {
|
|
|
|
|
+ return row?.categoryName || resolveFlowCategoryLabel(row?.category, categoryTreeOptions.value, '-') || '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getProcessDisplayName(task) {
|
|
|
|
|
+ return task?.processName || task?.processDefinitionName || task?.processTitle || task?.modelName || '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getSettleTypeLabel(settleType) {
|
|
|
|
|
+ const map = { NORMAL: '普通', DISCOUNT: '折扣', PROMOTION: '优惠' }
|
|
|
|
|
+ return map[settleType] || settleType || '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatMoney(value) {
|
|
|
|
|
+ if (value === null || value === undefined)
|
|
|
|
|
+ return '-'
|
|
|
|
|
+ return `¥${Number(value).toFixed(2)}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 从 businessKey 中提取订单ID(格式:discount_checkout:{orderId}) */
|
|
|
|
|
+function extractOrderId(businessKey) {
|
|
|
|
|
+ if (!businessKey)
|
|
|
|
|
+ return null
|
|
|
|
|
+ const parts = String(businessKey).split(':')
|
|
|
|
|
+ if (parts.length >= 2 && parts[1]) {
|
|
|
|
|
+ // 保持字符串格式,避免大数字精度丢失(雪花ID超过Number.MAX_SAFE_INTEGER)
|
|
|
|
|
+ return parts[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadOrderDetail(row) {
|
|
|
|
|
+ const orderId = extractOrderId(row?.businessKey)
|
|
|
|
|
+ if (!orderId) {
|
|
|
|
|
+ orderDetail.value = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ orderLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await request.get('/restaurant/dishOrder/detail', { params: { id: orderId } })
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ orderDetail.value = res.data || null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('加载订单明细失败', error)
|
|
|
|
|
+ orderDetail.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ orderLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadData() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await flowApi.getNotificationTasks({
|
|
|
|
|
+ pageNum: pagination.page,
|
|
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
|
|
+ userId: String(userStore.userId),
|
|
|
|
|
+ title: queryParams.title || undefined,
|
|
|
|
|
+ isRead: queryParams.isRead,
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ dataSource.value = res.data?.records || []
|
|
|
|
|
+ pagination.itemCount = res.data?.total || 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('加载通知列表失败', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadUnreadCount() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await flowApi.getUnreadNotificationCount(String(userStore.userId))
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ unreadCount.value = res.data || 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('加载未读数量失败', error)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleSearch() {
|
|
|
|
|
+ pagination.page = 1
|
|
|
|
|
+ loadData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleReset() {
|
|
|
|
|
+ queryParams.title = ''
|
|
|
|
|
+ queryParams.isRead = null
|
|
|
|
|
+ handleSearch()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleMarkAsRead(row) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await flowApi.markNotificationAsRead({
|
|
|
|
|
+ taskId: row.taskId || row.id,
|
|
|
|
|
+ userId: String(userStore.userId),
|
|
|
|
|
+ tenantId: userStore.tenantId,
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ window.$message?.success('已标记为已读')
|
|
|
|
|
+ row.isRead = 1
|
|
|
|
|
+ await loadUnreadCount()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('标记已读失败', error)
|
|
|
|
|
+ window.$message?.error('标记已读失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleMarkSelectedAsRead() {
|
|
|
|
|
+ if (selectedTaskKeys.value.length === 0) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (const key of selectedTaskKeys.value) {
|
|
|
|
|
+ const task = dataSource.value.find(t => t.id === key || t.taskId === key)
|
|
|
|
|
+ if (task && task.isRead !== 1) {
|
|
|
|
|
+ await flowApi.markNotificationAsRead({
|
|
|
|
|
+ taskId: task.taskId || task.id,
|
|
|
|
|
+ userId: String(userStore.userId),
|
|
|
|
|
+ tenantId: userStore.tenantId,
|
|
|
|
|
+ })
|
|
|
|
|
+ task.isRead = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ window.$message?.success(`已标记 ${selectedTaskKeys.value.length} 条为已读`)
|
|
|
|
|
+ selectedTaskKeys.value = []
|
|
|
|
|
+ await loadUnreadCount()
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('批量标记已读失败', error)
|
|
|
|
|
+ window.$message?.error('批量标记已读失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleMarkAllAsRead() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await flowApi.markAllNotificationsAsRead({
|
|
|
|
|
+ userId: String(userStore.userId),
|
|
|
|
|
+ tenantId: userStore.tenantId,
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ window.$message?.success('全部已标记为已读')
|
|
|
|
|
+ dataSource.value.forEach((task) => {
|
|
|
|
|
+ task.isRead = 1
|
|
|
|
|
+ })
|
|
|
|
|
+ unreadCount.value = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('全部标记已读失败', error)
|
|
|
|
|
+ window.$message?.error('全部标记已读失败')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function openDrawer(row) {
|
|
|
|
|
+ currentTask.value = row
|
|
|
|
|
+ approvalHistory.value = []
|
|
|
|
|
+ orderDetail.value = null
|
|
|
|
|
+ showDrawer.value = true
|
|
|
|
|
+
|
|
|
|
|
+ // 自动标记为已读
|
|
|
|
|
+ if (row.isRead !== 1) {
|
|
|
|
|
+ await handleMarkAsRead(row)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 加载流转记录
|
|
|
|
|
+ if (row.processInstanceId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await flowApi.getProcessHistory(row.processInstanceId)
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ approvalHistory.value = res.data || []
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.error('加载流转记录失败', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 加载订单明细(折扣/优惠结账通知)
|
|
|
|
|
+ const orderId = extractOrderId(row?.businessKey)
|
|
|
|
|
+ if (orderId) {
|
|
|
|
|
+ await loadOrderDetail(row)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ categoryTreeOptions.value = buildFlowCategoryTreeOptions([])
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ loadUnreadCount()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.notification-title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.unread-dot {
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background-color: #18a058;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.notification-unread {
|
|
|
|
|
+ background-color: #fffbe6;
|
|
|
|
|
+ color: #d48806;
|
|
|
|
|
+ border: 1px solid #ffe58f;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.notification-read {
|
|
|
|
|
+ background-color: #f5f5f5;
|
|
|
|
|
+ color: #8c8c8c;
|
|
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-list-hint.unread {
|
|
|
|
|
+ color: #18a058;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.notification-action-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ padding: 16px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dish-table-wrap {
|
|
|
|
|
+ margin: 16px 0;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dish-table {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ border-collapse: collapse;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dish-table th {
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ border-bottom: 2px solid #e2e8f0;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dish-table td {
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ border-bottom: 1px solid #f1f5f9;
|
|
|
|
|
+ color: #334155;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dish-table tbody tr:hover {
|
|
|
|
|
+ background: #f8fafc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-summary {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: #fafbfc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 6px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row:not(:last-child) {
|
|
|
|
|
+ border-bottom: 1px dashed #e2e8f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-label {
|
|
|
|
|
+ color: #64748b;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-value {
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row.discount .amount-label {
|
|
|
|
|
+ color: #f59e0b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row.discount .amount-value {
|
|
|
|
|
+ color: #f59e0b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row.total {
|
|
|
|
|
+ padding-top: 10px;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ border-top: 2px solid #e2e8f0;
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row.total .amount-label {
|
|
|
|
|
+ color: #1e293b;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.amount-row.total .amount-value {
|
|
|
|
|
+ color: #dc2626;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|