feat(m9): add subscription report config (localStorage MVP)

This commit is contained in:
2026-05-25 15:05:18 +08:00
parent 1cef437fb3
commit 250c5cbfeb
4 changed files with 129 additions and 0 deletions
@@ -136,6 +136,7 @@ const menuItems = [
{ path: "/devices", icon: "🖥️", label: "设备管理", roles: ["SYS_ADMIN","SALES","DELIVERY"] }, { path: "/devices", icon: "🖥️", label: "设备管理", roles: ["SYS_ADMIN","SALES","DELIVERY"] },
{ path: "/todos", icon: "🔔", label: "待办中心", roles: ["SYS_ADMIN","SALES","LICENSE_OPS"] }, { path: "/todos", icon: "🔔", label: "待办中心", roles: ["SYS_ADMIN","SALES","LICENSE_OPS"] },
{ path: "/reports/contract-sn", icon: "📊", label: "报表中心", roles: ["SYS_ADMIN"] }, { path: "/reports/contract-sn", icon: "📊", label: "报表中心", roles: ["SYS_ADMIN"] },
{ path: "/reports/subscriptions", icon: "📧", label: "报表订阅", roles: ["SYS_ADMIN"] },
]; ];
const visibleMenu = computed(() => menuItems.filter(m => auth.hasAnyRole(m.roles))); const visibleMenu = computed(() => menuItems.filter(m => auth.hasAnyRole(m.roles)));
@@ -188,6 +188,12 @@ const routes = [
component: () => import("../views/ProfileView.vue"), component: () => import("../views/ProfileView.vue"),
meta: { roles: ["SYS_ADMIN", "SALES", "LICENSE_OPS"], title: "个人设置" }, meta: { roles: ["SYS_ADMIN", "SALES", "LICENSE_OPS"], title: "个人设置" },
}, },
{
path: "reports/subscriptions",
name: "report-subscriptions",
component: () => import("../views/SubscriptionReportView.vue"),
meta: { roles: ["SYS_ADMIN"], title: "报表订阅" },
},
{ {
path: "reports/project-health", path: "reports/project-health",
name: "project-health", name: "project-health",
@@ -44,6 +44,7 @@ const allModuleLinks = [
{ to: "/devices", label: "设备管理", roles: ["SYS_ADMIN", "SALES", "DELIVERY"] }, { to: "/devices", label: "设备管理", roles: ["SYS_ADMIN", "SALES", "DELIVERY"] },
{ to: "/todos", label: "待办中心", roles: ["SYS_ADMIN", "SALES", "LICENSE_OPS"] }, { to: "/todos", label: "待办中心", roles: ["SYS_ADMIN", "SALES", "LICENSE_OPS"] },
{ to: "/reports/contract-sn", label: "报表中心", roles: ["SYS_ADMIN"] }, { to: "/reports/contract-sn", label: "报表中心", roles: ["SYS_ADMIN"] },
{ to: "/reports/subscriptions", label: "报表订阅", roles: ["SYS_ADMIN"] },
]; ];
const visibleModuleLinks = computed(() => allModuleLinks.filter((l) => auth.hasAnyRole(l.roles))); const visibleModuleLinks = computed(() => allModuleLinks.filter((l) => auth.hasAnyRole(l.roles)));
@@ -0,0 +1,121 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
const STORAGE_KEY = 'craftlabs_report_subscriptions'
const subscriptions = ref([])
const dialogVisible = ref(false)
const editingIndex = ref(-1)
const form = ref({ reportType: 'contract-sn', schedule: 'weekly', email: '', enabled: true })
function load() {
try {
const data = localStorage.getItem(STORAGE_KEY)
subscriptions.value = data ? JSON.parse(data) : []
} catch { subscriptions.value = [] }
}
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(subscriptions.value))
}
function openCreate() {
editingIndex.value = -1
form.value = { reportType: 'contract-sn', schedule: 'weekly', email: '', enabled: true }
dialogVisible.value = true
}
function openEdit(idx) {
editingIndex.value = idx
form.value = { ...subscriptions.value[idx] }
dialogVisible.value = true
}
function handleSave() {
if (editingIndex.value >= 0) {
subscriptions.value[editingIndex.value] = { ...form.value }
} else {
subscriptions.value.push({ ...form.value, id: Date.now() })
}
save()
dialogVisible.value = false
ElMessage.success('订阅已保存')
}
function handleDelete(idx) {
subscriptions.value.splice(idx, 1)
save()
ElMessage.success('订阅已删除')
}
function toggleEnabled(idx) {
subscriptions.value[idx].enabled = !subscriptions.value[idx].enabled
save()
}
onMounted(load)
</script>
<template>
<div>
<el-card shadow="never">
<template #header>
<div class="toolbar">
<span class="title">报表订阅</span>
<el-button type="success" @click="openCreate">新建订阅</el-button>
</div>
</template>
<el-empty v-if="subscriptions.length === 0" description="暂未配置报表订阅" />
<el-table v-else :data="subscriptions" stripe>
<el-table-column label="报表类型" width="160">
<template #default="{ row }">
{{ row.reportType === 'contract-sn' ? '履约对账' : row.reportType === 'callback-stats' ? 'Callback 统计' : row.reportType }}
</template>
</el-table-column>
<el-table-column prop="schedule" label="推送频率" width="120" />
<el-table-column prop="email" label="接收邮箱" min-width="200" />
<el-table-column label="启用" width="80">
<template #default="{ row, $index }">
<el-switch :model-value="row.enabled" @change="toggleEnabled($index)" />
</template>
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="{ $index }">
<el-button type="primary" link @click="openEdit($index)">编辑</el-button>
<el-button type="danger" link @click="handleDelete($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="editingIndex >= 0 ? '编辑订阅' : '新建订阅'" width="480px">
<el-form label-width="100px">
<el-form-item label="报表类型" required>
<el-select v-model="form.reportType" style="width:100%">
<el-option label="履约对账" value="contract-sn" />
<el-option label="Callback 统计" value="callback-stats" />
</el-select>
</el-form-item>
<el-form-item label="推送频率" required>
<el-select v-model="form.schedule" style="width:100%">
<el-option label="每日" value="daily" />
<el-option label="每周" value="weekly" />
<el-option label="每月" value="monthly" />
</el-select>
</el-form-item>
<el-form-item label="接收邮箱" required>
<el-input v-model="form.email" placeholder="email@example.com" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.toolbar { display: flex; justify-content: space-between; align-items: center; }
.title { font-weight: 600; font-size: 16px; }
</style>