Files
oa_app/pages/tabs/workbench.vue
T
2026-09-23 16:52:48 +08:00

217 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<fy-header title="工作台">
<template #right>
<text class="ico" @click="goSearch"></text>
</template>
</fy-header>
<view class="body">
<view v-if="announce" class="notice" @click="openNotice">
<text class="nlab">公告 · {{ announce }}</text>
<text class="arr"></text>
</view>
<view class="card apps">
<view class="cap">
<text>常用业务入口</text>
<text class="cap-sub more" @click="goApply">全部流程 </text>
</view>
<view v-if="!flows.length" class="empty">暂无可用审批流请确认账号已开通申请权限</view>
<view class="grid">
<view v-for="a in flows" :key="a.key" class="app" @click="openFlow(a)">
<view class="icon"><text>{{ a.emoji }}</text></view>
<text class="al">{{ a.label }}</text>
<text v-if="a.desc" class="ad">{{ a.desc }}</text>
</view>
</view>
</view>
<view v-if="apps.length" class="card apps">
<view class="cap">
<text>个人办公</text>
<text class="cap-sub">按你的权限显示</text>
</view>
<view class="grid">
<view v-for="a in apps" :key="a.id" class="app" @click="open(a)">
<view class="icon">
<text>{{ a.icon }}</text>
<text v-if="a.badge === 'todo' && todoCount" class="badge abs">{{ todoCount }}</text>
</view>
<text class="al">{{ a.title }}</text>
</view>
</view>
</view>
<view v-if="erp.length" class="card mods">
<view class="cap">
<text>管理应用</text>
<text class="cap-sub">Web 授权后才会出现</text>
</view>
<view v-for="m in erp" :key="m.id" class="mod" @click="open(m)">
<text class="mi">{{ m.icon }}</text>
<text class="mt">{{ m.title }}</text>
<text class="arr"></text>
</view>
</view>
<view class="todo-head">
<view class="th">
<text>我的待办</text>
<text v-if="todoCount" class="badge">{{ todoCount }}</text>
</view>
<text class="more" @click="goTodo">查看全部 </text>
</view>
<view v-if="!pending.length" class="empty">暂无待办</view>
<view v-for="t in pending" :key="pendKey(t)" class="card todo" @click="openPending(t)">
<view class="av">{{ (t.submitter || t.applicant || '?').slice(0, 1) }}</view>
<view class="tm">
<text class="tn">{{ t.title || t.no }}</text>
<text class="ts">{{ t.tag || t.typeLabel || t.type || '流程审批' }} · {{ t.submitter || '' }}</text>
</view>
<text class="chip">{{ t.btn || '去办理' }}</text>
</view>
</view>
</view>
</template>
<script>
import FyHeader from '../../components/fy-header.vue'
import { store } from '../../utils/store.js'
import { toast } from '../../utils/format.js'
import { oaPost } from '../../utils/api.js'
import { visibleApplyKinds } from '../../utils/apply.js'
import { openHref, tileEmoji } from '../../utils/nav.js'
export default {
components: { FyHeader },
data() {
return { apps: [], erp: [], todos: [], pendingItems: [], shortcuts: [], todoCount: 0, announce: '', off: null }
},
computed: {
flows() {
const fromApi = (this.shortcuts || []).map((s, i) => ({
key: 'sc-' + (s.to || s.label || i),
label: s.label || s.title || '入口',
desc: s.desc || '',
to: s.to || s.path || s.url || '',
emoji: tileEmoji(s)
})).filter((s) => s.to)
if (fromApi.length) return fromApi
const me = store.me || {}
if (!store.can('personal', 'apply')) return []
return visibleApplyKinds({
employeeStatus: me.employeeStatus,
isSuper: me.isSuper,
role: me.title
}).map((k) => ({
key: 'ak-' + k.kind,
label: k.title,
desc: k.hint,
to: '/pages/apply/form?kind=' + k.kind,
emoji: tileEmoji({ id: k.kind, title: k.title })
}))
},
pending() {
const mixed = this.pendingItems && this.pendingItems.length ? this.pendingItems : this.todos
return (mixed || []).slice(0, 8)
}
},
onShow() {
if (this.off) this.off()
if (store.phase !== 'app') uni.reLaunch({ url: '/pages/login/login' })
this.pull()
store.refreshPerms().then(() => {
this.pull()
return store.refreshWorkbench()
}).then(() => this.pull())
this.off = store.on(() => this.pull())
},
onHide() {
if (this.off) this.off()
this.off = null
},
methods: {
pull() {
this.apps = store.personalApps()
this.erp = store.erpModules()
this.todos = store.todos || []
this.pendingItems = store.pendingItems || []
this.shortcuts = store.shortcuts || []
this.todoCount = store.todoCount || 0
const a = (store.announcements || [])[0]
this.announce = a ? (a.title || a.content || '') : ''
},
open(item) {
if (!item || !item.url) return
if (item.url.startsWith('/pages/tabs/')) {
uni.switchTab({ url: item.url.split('?')[0] })
return
}
uni.navigateTo({ url: item.url })
},
openFlow(a) {
oaPost('/dashboard/shortcuts/hit', { path: a.to }, store.token).catch(() => {})
if (!openHref(a.to)) toast('该入口暂未接入手机端')
},
goSearch() { uni.navigateTo({ url: '/pages/search/search' }) },
goApply() {
if (store.can('personal', 'apply')) uni.navigateTo({ url: '/pages/apply/list' })
else toast('没有发起申请权限')
},
goTodo() { uni.navigateTo({ url: '/pages/todo/list' }) },
pendKey(t) { return t.no || t.id || t.to || t.title },
openPending(t) {
if (t.source === 'work' || t.taskId) {
uni.navigateTo({ url: '/pages/coop/detail?id=' + encodeURIComponent(t.id || t.taskId) })
return
}
const no = t.no || t.requestNo || t.id
if (no && (t.type || t.typeLabel || t.source === 'workflow' || !t.to || String(t.to).startsWith('/todo'))) {
uni.navigateTo({ url: '/pages/todo/detail?no=' + encodeURIComponent(no) })
return
}
if (openHref(t.to, t)) return
if (no) uni.navigateTo({ url: '/pages/todo/detail?no=' + encodeURIComponent(no) })
},
openNotice() { uni.navigateTo({ url: '/pages/notice/list' }) }
}
}
</script>
<style scoped>
.body { padding: 16rpx 24rpx 40rpx; }
.notice {
background: rgba(219, 225, 255, 0.4);
border: 1rpx solid #dbe1ff;
border-radius: 16rpx;
padding: 20rpx 24rpx;
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 20rpx;
}
.nlab { font-size: 24rpx; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.arr { color: #737686; }
.apps, .mods { padding: 24rpx; margin-bottom: 20rpx; }
.cap { display: flex; justify-content: space-between; align-items: center; font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx; border-bottom: 1rpx solid #eaedff; padding-bottom: 16rpx; }
.cap-sub { font-size: 22rpx; color: #737686; font-weight: 400; }
.grid { display: flex; flex-wrap: wrap; }
.app { width: 25%; display: flex; flex-direction: column; align-items: center; padding: 16rpx 4rpx; box-sizing: border-box; }
.icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: #f2f3ff; display: flex; align-items: center; justify-content: center; position: relative; margin-bottom: 8rpx; font-size: 32rpx; }
.abs { position: absolute; top: -8rpx; right: -8rpx; }
.al { font-size: 22rpx; text-align: center; }
.ad { font-size: 18rpx; color: #737686; text-align: center; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mod { display: flex; align-items: center; padding: 20rpx 0; border-top: 1rpx solid #eaedff; }
.mi { width: 48rpx; }
.mt { flex: 1; font-size: 28rpx; }
.todo-head { display: flex; justify-content: space-between; align-items: center; margin: 16rpx 8rpx; }
.th { display: flex; align-items: center; gap: 10rpx; font-size: 30rpx; font-weight: 600; }
.more { font-size: 22rpx; color: #434655; }
.todo { display: flex; align-items: center; padding: 24rpx; margin-bottom: 16rpx; }
.av { width: 80rpx; height: 80rpx; border-radius: 50%; background: #dbe1ff; color: #004ac6; display: flex; align-items: center; justify-content: center; font-weight: 700; }
.tm { flex: 1; margin: 0 16rpx; min-width: 0; }
.tn { display: block; font-size: 28rpx; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts { font-size: 22rpx; color: #737686; }
.empty { padding: 24rpx; text-align: center; color: #737686; font-size: 24rpx; }
.ico { padding: 8rpx 16rpx; }
</style>