gongtianxiao 1 mese fa
parent
commit
862703a1a9

+ 0 - 6
src/api/enterpriseList.js

@@ -52,12 +52,6 @@ export async function submitNewAsset(patch, { fillTime = Date.now() } = {}) {
   const id = res.content;
   if (!id) throw new Error('新增失败:未返回内容 id');
 
-  await updateAudit({
-    id,
-    columnId: COLUMN_ID,
-    state: 1,
-  });
-
   return { id, contentBody };
 }
 

+ 2 - 0
src/assets/config.js

@@ -1,4 +1,6 @@
+// export const BASE_URL = 'http://121.43.55.7:10081/dms';
 export const BASE_URL = 'http://121.43.55.7:2101';
+
 export const LOGIN_URL = 'http://121.43.55.7:10086';
 
 //模型ID

+ 62 - 23
src/components/layout/NotificationModal.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <Teleport to="body">
     <div v-if="visible" class="notify-overlay" @click.self="close">
       <div
@@ -20,11 +20,19 @@
               variant="outline"
               @click="markAllRead"
             />
+            <BaseButton
+              v-if="displayList.length > 0"
+              text="全部删除"
+              size="sm"
+              variant="outline"
+              class="notify-btn--danger"
+              @click="handleDeleteAll"
+            />
             <BaseButton text="刷新" size="sm" variant="outline" :disabled="loading" @click="refresh" />
           </div>
         </div>
 
-        <div v-if="loading" class="notify-empty">加载中…</div>
+        <div v-if="loading && displayList.length === 0" class="notify-empty">加载中…</div>
         <div v-else-if="displayList.length === 0" class="notify-empty">暂无消息</div>
 
         <div v-else class="notify-table-wrap">
@@ -68,6 +76,15 @@
                 />
                 <span v-else class="muted">—</span>
               </span>
+              <span :style="{ width: notifyColumns[5].width }" @click.stop>
+                <BaseButton
+                  text="删除"
+                  size="sm"
+                  variant="outline"
+                  class="notify-btn--danger"
+                  @click="handleDeleteOne(item.id)"
+                />
+              </span>
             </div>
           </div>
         </div>
@@ -81,13 +98,15 @@
 </template>
 
 <script setup>
-import { computed, ref, watch } from 'vue';
+import { showConfirm } from '@/utils/message';
+import { computed, ref, watch,defineProps,defineEmits } from 'vue';
 import { useRouter } from 'vue-router';
 import BaseButton from '@/components/base/BaseButton.vue';
 import { useEnterpriseStore } from '@/store/enterprise';
+import { useFilterStore } from '@/store/filter';
 import { useNotificationStore } from '@/store/notification';
 import { buildNotifications } from '@/utils/buildNotifications';
-import {defineProps, defineEmits} from 'vue';
+
 const props = defineProps({
   visible: {
     type: Boolean,
@@ -99,16 +118,18 @@ const emit = defineEmits(['update:visible', 'unread-change']);
 
 const router = useRouter();
 const enterpriseStore = useEnterpriseStore();
+const filterStore = useFilterStore();
 const notificationStore = useNotificationStore();
 
 const loading = ref(false);
 
 const notifyColumns = [
-  { key: 'time', label: '时间', width: '160px' },
-  { key: 'type', label: '类型', width: '100px' },
-  { key: 'title', label: '标题', width: '360px' },
-  { key: 'status', label: '状态', width: '80px' },
-  { key: 'action', label: '操作', width: '120px' },
+  { key: 'time', label: '时间', width: '150px' },
+  { key: 'type', label: '类型', width: '90px' },
+  { key: 'title', label: '标题', width: '320px' },
+  { key: 'status', label: '状态', width: '70px' },
+  { key: 'action', label: '操作', width: '100px' },
+  { key: 'del', label: '', width: '40px' },
 ];
 
 const TYPE_CLASS = {
@@ -120,34 +141,37 @@ const TYPE_CLASS = {
   租约预警: 'orange',
 };
 
-const rawNotifications = computed(() =>
-  buildNotifications({
-    pendingList: enterpriseStore.pendingList,
-    approvedList: enterpriseStore.approvedList,
-    rejectedList: enterpriseStore.rejectedList,
-  })
-);
+/** 当前管理集团 */
+const currentGroup = computed(() => filterStore.selectManageGroup);
 
+/** 从持久化 store 读取,按当前集团过滤 */
 const displayList = computed(() =>
-  rawNotifications.value.map((item) => ({
+  notificationStore.itemsByGroup(currentGroup.value).map((item) => ({
     ...item,
-    read: notificationStore.isRead(item.id),
     typeClass: TYPE_CLASS[item.type] || 'gray',
   }))
 );
 
+/** 当前集团的未读数 */
 const unreadCount = computed(() =>
-  displayList.value.filter((item) => !item.read).length
+  notificationStore.unreadCountByGroup(currentGroup.value)
 );
 
 watch(unreadCount, (count) => {
   emit('unread-change', count);
 }, { immediate: true });
 
+/** 拉取最新数据并生成通知(不对集团做过滤,保证全集团通知都持久化) */
 const refresh = async () => {
   loading.value = true;
   try {
     await enterpriseStore.fetchReportProgress({ force: true, skipSubmitted: true });
+    const newItems = buildNotifications({
+      pendingList: enterpriseStore.pendingList,
+      approvedList: enterpriseStore.approvedList,
+      rejectedList: enterpriseStore.rejectedList,
+    });
+    notificationStore.mergeNotifications(newItems);
   } finally {
     loading.value = false;
   }
@@ -162,7 +186,18 @@ const markRead = (id) => {
 };
 
 const markAllRead = () => {
-  notificationStore.markAllRead(displayList.value.map((item) => item.id));
+  const ids = displayList.value.map((item) => item.id);
+  notificationStore.markAllRead(ids);
+};
+
+const handleDeleteOne = async (id) => {
+  if (!(await showConfirm('确认删除该通知?'))) return;
+  notificationStore.deleteItem(id);
+};
+
+const handleDeleteAll = async () => {
+  if (!(await showConfirm('确认删除全部通知?'))) return;
+  notificationStore.deleteAll();
 };
 
 const goTo = (item) => {
@@ -183,7 +218,6 @@ const handleRowClick = (item) => {
 watch(
   () => props.visible,
   (open) => {
-    document.body.style.overflow = open ? 'hidden' : '';
     if (open) refresh();
   }
 );
@@ -203,7 +237,7 @@ watch(
 
 .notify-modal {
   width: 100%;
-  max-width: 920px;
+  max-width: 960px;
   max-height: calc(100vh - 48px);
   overflow: auto;
   padding: 24px 28px;
@@ -249,6 +283,11 @@ watch(
   flex-shrink: 0;
 }
 
+.notify-btn--danger {
+  color: rgba(220, 50, 30, 1);
+  border-color: rgba(220, 50, 30, 0.4);
+}
+
 .notify-empty {
   min-height: 120px;
   display: flex;
@@ -274,7 +313,7 @@ watch(
 .data-table__row {
   display: flex;
   align-items: center;
-  gap: 12px;
+  gap: 8px;
   min-height: 42px;
 }
 

+ 13 - 2
src/components/layout/PageHeader.vue

@@ -51,13 +51,16 @@ import BaseSelect from '@/components/base/BaseSelect.vue';
 import NotificationModal from '@/components/layout/NotificationModal.vue';
 import { useEnterpriseStore } from '@/store/enterprise';
 import { useFilterStore } from '@/store/filter';
+import { useNotificationStore } from '@/store/notification';
 import { useUserStore } from '@/store/user';
 import { GROUP_ORDER } from '@/utils/aggregateReportRecords';
+import { buildNotifications } from '@/utils/buildNotifications';
 
 const route = useRoute();
 const router = useRouter();
 const enterpriseStore = useEnterpriseStore();
 const filterStore = useFilterStore();
+const notificationStore = useNotificationStore();
 const userStore = useUserStore();
 
 const showNotify = ref(false);
@@ -89,8 +92,16 @@ const goManage = () => {
   router.push('/manage/dashboard');
 };
 
-onMounted(() => {
-  enterpriseStore.fetchReportProgress({ skipSubmitted: true });
+onMounted(async () => {
+  await enterpriseStore.fetchReportProgress({ skipSubmitted: true });
+  // 首次加载后生成通知并入库
+  notificationStore.mergeNotifications(
+    buildNotifications({
+      pendingList: enterpriseStore.pendingList,
+      approvedList: enterpriseStore.approvedList,
+      rejectedList: enterpriseStore.rejectedList,
+    })
+  );
 });
 </script>
 

+ 79 - 25
src/store/notification.js

@@ -17,48 +17,102 @@ const resolveUserKey = () => {
 
 export const useNotificationStore = defineStore('notification', {
   state: () => ({
-    readIdsByUser: {},
+    /** { [userKey]: notification[] } */
+    itemsByUser: {},
   }),
   getters: {
-    readIds(state) {
+    /** 当前用户的所有通知(按时间倒序) */
+    items(state) {
       const key = resolveUserKey();
-      return state.readIdsByUser[key] || [];
+      return (state.itemsByUser[key] || []).slice().sort((a, b) => b.timestamp - a.timestamp);
     },
-    unreadCount(state) {
-      return (notificationIds = []) => {
-        const readSet = new Set(state.readIdsByUser[resolveUserKey()] || []);
-        return notificationIds.filter((id) => !readSet.has(id)).length;
+    /** 按集团过滤 */
+    itemsByGroup(state) {
+      return (group) => {
+        const all = (state.itemsByUser[resolveUserKey()] || []).slice();
+        if (!group) return all.sort((a, b) => b.timestamp - a.timestamp);
+        return all
+          .filter((item) => !item.group || item.group === group)
+          .sort((a, b) => b.timestamp - a.timestamp);
       };
     },
-    isRead(state) {
-      return (id) => {
-        const readSet = new Set(state.readIdsByUser[resolveUserKey()] || []);
-        return readSet.has(id);
+    /** 按集团的未读数 */
+    unreadCountByGroup(state) {
+      return (group) => {
+        const all = state.itemsByUser[resolveUserKey()] || [];
+        const filtered = group
+          ? all.filter((item) => (!item.group || item.group === group) && !item.read)
+          : all.filter((item) => !item.read);
+        return filtered.length;
       };
     },
   },
   actions: {
-    setReadIdsForKey(key, ids) {
-      this.readIdsByUser = {
-        ...this.readIdsByUser,
-        [key]: ids,
-      };
+    _getItems() {
+      const key = resolveUserKey();
+      if (!this.itemsByUser[key]) {
+        this.itemsByUser = { ...this.itemsByUser, [key]: [] };
+      }
+      return this.itemsByUser[key];
     },
-    markRead(id) {
+    _setItems(arr) {
       const key = resolveUserKey();
-      const ids = this.readIdsByUser[key] || [];
-      if (ids.includes(id)) return;
-      this.setReadIdsForKey(key, [...ids, id]);
+      this.itemsByUser = { ...this.itemsByUser, [key]: arr };
+    },
+
+    /**
+     * 合并新生成的通知到持久化存储
+     * - 已存在的(同 id)保留原样(保留已读/已删除状态)
+     * - 新增的追加到列表
+     */
+    mergeNotifications(newItems) {
+      if (!newItems?.length) return;
+      const items = [...this._getItems()];
+      const existingIds = new Set(items.map((i) => i.id));
+      let added = 0;
+      newItems.forEach((item) => {
+        if (!existingIds.has(item.id)) {
+          items.push(item);
+          existingIds.add(item.id);
+          added++;
+        }
+      });
+      if (added > 0) {
+        this._setItems(items);
+      }
+    },
+
+    markRead(id) {
+      const items = this._getItems();
+      const index = items.findIndex((i) => i.id === id);
+      if (index < 0) return;
+      const updated = [...items];
+      updated[index] = { ...updated[index], read: true };
+      this._setItems(updated);
     },
+
     markAllRead(ids) {
-      const key = resolveUserKey();
-      const existing = new Set(this.readIdsByUser[key] || []);
-      ids.forEach((id) => existing.add(id));
-      this.setReadIdsForKey(key, [...existing]);
+      if (!ids?.length) return;
+      const idSet = new Set(ids);
+      const updated = this._getItems().map((item) =>
+        idSet.has(item.id) ? { ...item, read: true } : item
+      );
+      this._setItems(updated);
+    },
+
+    /** 删除单条通知 */
+    deleteItem(id) {
+      const items = this._getItems().filter((i) => i.id !== id);
+      this._setItems(items);
+    },
+
+    /** 删除当前用户全部通知 */
+    deleteAll() {
+      this._setItems([]);
     },
   },
   persist: {
     key: 'guoziwei-notification',
-    pick: ['readIdsByUser'],
+    pick: ['itemsByUser'],
   },
 });

+ 2 - 2
src/utils/assetDetailFields.js

@@ -10,8 +10,8 @@ export const ASSET_DETAIL_FIELDS = [
     label: '所属青浦集团',
     key: 'c_ssqpjt',
     required: true,
-    readonly: true,
-    placeholder: '根据所属集团自动匹配所属青浦集团',
+    // readonly: true,
+    placeholder: '请输入所属青浦集团',
   },
   {
     label: '所属集团',

+ 34 - 5
src/utils/buildNotifications.js

@@ -60,7 +60,7 @@ const groupHasRejected = (rejectedList, group, period) =>
 
 const buildRoute = (tab, query = {}) => buildManageRoute({ tab, ...query });
 
-/** 通知 id 仅依赖账期 + 集团,避免 record.id 因字段补全而变化 */
+/** 通知 id 仅依赖类型 + 账期 + 集团,保证同一条通知不会重复入库 */
 const notifyRecordId = (type, record) => {
   const qingpuGroup = (record.qingpuGroup ?? '').trim() || 'unknown';
   const group = (record.group ?? '').trim() || 'unknown';
@@ -68,13 +68,17 @@ const notifyRecordId = (type, record) => {
 };
 
 /**
+ * 生成当前时刻所有适用的通知(幂等,同 id 不会重复入库)
  * @param {{ pendingList: object[], approvedList: object[], rejectedList: object[] }} lists
- * @returns {Array<{ id: string, time: string, type: string, title: string, timestamp: number, route?: object }>}
+ * @param {{ currentGroup?: string }} options - 可选,限定集团范围
+ * @returns {Array<{ id: string, time: string, type: string, title: string, timestamp: number, route?: object, group: string }>}
  */
 export const buildNotifications = ({
   pendingList = [],
   approvedList = [],
   rejectedList = [],
+} = {}, {
+  currentGroup = '',
 } = {}) => {
   const notifications = [];
   const now = Date.now();
@@ -82,8 +86,11 @@ export const buildNotifications = ({
   const deadline = getPeriodDeadlineDate(currentPeriod);
   const daysLeft = Math.ceil((deadline.getTime() - now) / (86400000));
 
+  // ── 待审核(来自 pendingList)─────────────────────────────
   aggregateReportRecords(pendingList, { fallbackPeriod: currentPeriod }).forEach((record) => {
-    const group = record.group || record.qingpuGroup;
+    const group = record.qingpuGroup || record.group;
+    if (currentGroup && group !== currentGroup) return;
+
     const latest = record.items?.reduce((best, item) => {
       const t = toTimestamp(getFillTime(item));
       return t >= toTimestamp(getFillTime(best)) ? item : best;
@@ -94,6 +101,7 @@ export const buildNotifications = ({
       type: '待审核',
       title: `${group} ${record.period} 资产月报(${record.count} 条)待审核`,
       timestamp: toTimestamp(getFillTime(latest)),
+      group,
       route: buildRoute('audit', {
         sub: 'pending',
         group: record.qingpuGroup || group,
@@ -102,8 +110,11 @@ export const buildNotifications = ({
     });
   });
 
+  // ── 已通过(来自 approvedList)────────────────────────────
   aggregateReportRecords(approvedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
-    const group = record.group || record.qingpuGroup;
+    const group = record.qingpuGroup || record.group;
+    if (currentGroup && group !== currentGroup) return;
+
     const latest = record.items?.reduce((best, item) => {
       const t = toTimestamp(getAuditTime(item));
       return t >= toTimestamp(getAuditTime(best)) ? item : best;
@@ -114,6 +125,7 @@ export const buildNotifications = ({
       type: '审核结果',
       title: `${record.period} 月报已通过(${group},${record.count} 条)`,
       timestamp: toTimestamp(getAuditTime(latest)),
+      group,
       route: buildRoute('archive', {
         sub: 'history',
         group: record.qingpuGroup || group,
@@ -123,8 +135,11 @@ export const buildNotifications = ({
     });
   });
 
+  // ── 已驳回(来自 rejectedList)────────────────────────────
   aggregateReportRecords(rejectedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
-    const group = record.group || record.qingpuGroup;
+    const group = record.qingpuGroup || record.group;
+    if (currentGroup && group !== currentGroup) return;
+
     const latest = record.items?.reduce((best, item) => {
       const t = toTimestamp(getAuditTime(item));
       return t >= toTimestamp(getAuditTime(best)) ? item : best;
@@ -136,6 +151,7 @@ export const buildNotifications = ({
       type: '审核结果',
       title: `${record.period} 月报已被驳回(${group})${reason ? `:${reason}` : ''}`,
       timestamp: toTimestamp(getAuditTime(latest)),
+      group,
       route: buildRoute('archive', {
         sub: 'history',
         group: record.qingpuGroup || group,
@@ -145,9 +161,11 @@ export const buildNotifications = ({
     });
   });
 
+  // ── 填报提醒(当前账期截止前 7 天,尚未提交的集团)────────
   if (daysLeft > 0 && daysLeft <= 7) {
     const allLists = [pendingList, approvedList, rejectedList];
     GROUP_ORDER.forEach((group) => {
+      if (currentGroup && group !== currentGroup) return;
       if (groupHasAnySubmission(allLists, group, currentPeriod)) return;
       notifications.push({
         id: `reminder:${currentPeriod}:${group}`,
@@ -155,15 +173,19 @@ export const buildNotifications = ({
         type: '填报提醒',
         title: `${currentPeriod} 月报截止倒计时 ${daysLeft} 天(${group})`,
         timestamp: now,
+        group,
         route: buildRoute('archive'),
       });
     });
   }
 
+  // ── 逾期催办(上月截止后未通过/未提交的集团)──────────────
   const prevPeriod = getPreviousPeriod(currentPeriod);
   const prevDeadline = getPeriodDeadlineDate(prevPeriod);
   if (now > prevDeadline.getTime()) {
     GROUP_ORDER.forEach((group) => {
+      if (currentGroup && group !== currentGroup) return;
+
       if (groupHasApproved(approvedList, group, prevPeriod)) return;
 
       const rejected = groupHasRejected(rejectedList, group, prevPeriod);
@@ -180,6 +202,7 @@ export const buildNotifications = ({
           type: '逾期催办',
           title: `${prevPeriod} 月报已逾期,请补报(${group})`,
           timestamp: prevDeadline.getTime(),
+          group,
           route: buildRoute('archive'),
         });
         return;
@@ -192,12 +215,14 @@ export const buildNotifications = ({
           type: '逾期催办',
           title: `${prevPeriod} 月报被驳回且未重报,请尽快处理(${group})`,
           timestamp: now,
+          group,
           route: buildRoute('archive', { sub: 'history', group, period: prevPeriod, status: '已驳回' }),
         });
       }
     });
   }
 
+  // ── 租约到期/预警(来自已通过资产的 status 字段)──────────
   const expiryItems = approvedList
     .filter((item) => item.status === 'red' || item.status === 'orange')
     .slice(0, 8);
@@ -205,12 +230,16 @@ export const buildNotifications = ({
   expiryItems.forEach((item) => {
     const label = item.c_bh || item.c_qymc || '资产';
     const withinOneMonth = item.status === 'red';
+    const group = normalizeGroupName(item.c_ssqpjt) || '未知';
+    if (currentGroup && group !== currentGroup) return;
+
     notifications.push({
       id: `expiry:${item.id}`,
       time: formatDateTime(item.update_time || now),
       type: withinOneMonth ? '租约到期' : '租约预警',
       title: `${label} 租约${withinOneMonth ? '将于一个月内到期' : '将于三个月内到期'},请关注`,
       timestamp: toTimestamp(item.update_time || now),
+      group,
       route: buildRoute('assets'),
     });
   });

+ 21 - 12
src/utils/manageContext.js

@@ -1,3 +1,5 @@
+import { useFilterStore } from '@/store/filter';
+
 const DEFAULT_MANAGE_ENTERPRISE = '上海青浦新城发展(集团)有限公司';
 
 /** 默认当前企业对应的青浦集团(c_ssqpjt) */
@@ -10,20 +12,27 @@ export const resolveManageEnterprise = (userInfo) =>
   || DEFAULT_MANAGE_ENTERPRISE;
 
 /** 资产是否属于当前企业/集团管辖范围(新增上报资产弹窗用) */
-export const assetBelongsToManageScope = (item, enterprise) => {
-  const target = String(enterprise ?? '').trim();
-  if (!target) return true;
-
-  const qymc = String(item.c_qymc ?? '').trim();
-  const ssjt = String(item.c_ssjt ?? '').trim();
-  const qpGroup = String(item.c_ssqpjt ?? '').trim();
-
-  if (qymc === target || ssjt === target) return true;
-  if (qymc.includes(target) || target.includes(qymc)) return true;
-
-  if (target === DEFAULT_MANAGE_ENTERPRISE && qpGroup === DEFAULT_MANAGE_GROUP) {
+export const assetBelongsToManageScope = (item) => {
+  // 优先以所属青浦集团(c_ssqpjt)匹配当前管理集团
+  const filterStore = useFilterStore();
+  const qpGroup = String(item?.c_ssqpjt ?? '').trim();
+  if (qpGroup && qpGroup === filterStore.selectManageGroup) {
     return true;
   }
 
+  // // 原逻辑(注释保留)
+  // const target = String(enterprise ?? '').trim();
+  // if (!target) return true;
+  //
+  // const qymc = String(item.c_qymc ?? '').trim();
+  // const ssjt = String(item.c_ssjt ?? '').trim();
+  //
+  // if (qymc === target || ssjt === target) return true;
+  // if (qymc.includes(target) || target.includes(qymc)) return true;
+  //
+  // if (target === DEFAULT_MANAGE_ENTERPRISE && qpGroup === DEFAULT_MANAGE_GROUP) {
+  //   return true;
+  // }
+
   return false;
 };