init: 数字员工平台初始代码

包含前端(Vue3 + VueFlow 画布)、后端(Go)、文档体系。
- 工作台画布:节点拖放、连线模式、右键菜单、AI 助手
- 后端:连接器 API、专员种子数据
- 导航:左侧导航、工坊、市场、控制台
This commit is contained in:
eaiadmin
2026-08-18 20:19:58 +08:00
commit 4e8817d768
239 changed files with 48631 additions and 0 deletions
@@ -0,0 +1,159 @@
<template>
<div class="page-container">
<div class="page-title">消息通知</div>
<el-card shadow="never">
<template #header>
<div class="card-head">
<span>共 {{ items.length }} 条通知</span>
<el-button type="primary" size="small" plain @click="readAll">全部标记已读</el-button>
</div>
</template>
<el-empty v-if="!loading && items.length === 0" description="暂无通知" />
<div v-else v-loading="loading">
<div
v-for="n in items"
:key="n.id"
class="notice-item"
:class="{ unread: !n.read }"
@click="openNotice(n)"
>
<div class="notice-dot" :class="{ dot: !n.read }" />
<div class="notice-main">
<div class="notice-title">
<span class="title">{{ n.title }}</span>
<el-tag size="small" effect="plain" :type="typeTag(n.type)">{{ typeLabel(n.type) }}</el-tag>
<span class="time">{{ fmtTime(n.created_at) }}</span>
</div>
<div class="content">{{ n.content }}</div>
</div>
<el-button v-if="!n.read" type="primary" link size="small" @click.stop="markOne(n)">标为已读</el-button>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { myNotifications, markRead, markAllRead } from '@/api/notification'
import { useNotificationStore } from '@/store/notification'
const router = useRouter()
const notifStore = useNotificationStore()
const loading = ref(false)
const items = ref([])
function fmtTime(t) {
if (!t) return '—'
return String(t).replace('T', ' ').slice(0, 16)
}
function typeLabel(t) {
return { exam_publish: '考试发布', exam_pass: '考试通过', position_set: '岗位设置' }[t] || '通知'
}
function typeTag(t) {
return { exam_publish: 'warning', exam_pass: 'success', position_set: 'primary' }[t] || 'info'
}
function openNotice(n) {
if (!n.read) markOne(n)
if (n.link) router.push(n.link)
}
async function markOne(n) {
try {
await markRead(n.id)
n.read = true
notifStore.refresh()
} catch (e) {
// 忽略
}
}
async function readAll() {
try {
await markAllRead()
items.value.forEach((n) => (n.read = true))
notifStore.setUnread(0)
} catch (e) {
// 拦截器已提示
}
}
async function load() {
loading.value = true
try {
const res = await myNotifications()
items.value = res.data || []
} catch (e) {
items.value = []
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<style scoped>
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.notice-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 14px 8px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
}
.notice-item:last-child {
border-bottom: none;
}
.notice-item:hover {
background: #f8f9fb;
}
.notice-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: 6px;
flex: none;
}
.notice-dot.dot {
background: #f56c6c;
}
.notice-main {
flex: 1;
min-width: 0;
}
.notice-title {
display: flex;
align-items: center;
gap: 8px;
}
.title {
font-weight: 600;
color: #303133;
}
.notice-item.unread .title {
color: #303133;
}
.time {
margin-left: auto;
font-size: 12px;
color: #c0c4cc;
}
.content {
font-size: 13px;
color: #606266;
margin-top: 4px;
}
</style>