Files
oa_app/pages/tabs/workbench.vue
T
2026-09-24 09:54:39 +08:00

273 lines
11 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 edge">
<fy-header :title="greet">
<template #right>
<text class="ico" @click="goSearch">搜索</text>
</template>
</fy-header>
<fy-scroll class="edge-scroll" dock @refresh="reload">
<view class="body">
<view class="hello">
<text class="hello-sub">今天也要高效完成工作</text>
</view>
<view class="stats">
<view class="stat" @click="goTodo">
<text class="sn">{{ todoCount }}</text>
<text class="sl">待办</text>
</view>
<view class="stat" @click="goApply">
<text class="sn">{{ flows.length }}</text>
<text class="sl">可发起</text>
</view>
<view class="stat" @click="openNotice">
<text class="sn">{{ apps.length }}</text>
<text class="sl">办公应用</text>
</view>
</view>
<view v-if="announce" class="notice" @click="openNotice">
<text class="ntag">公告</text>
<text class="nlab">{{ announce }}</text>
<text class="arr">›</text>
</view>
<view class="sec-h">
<text class="sec-t">快捷办公</text>
<text class="more" @click="goApply">全部流程</text>
</view>
<view class="card apps">
<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" :style="{ background: tileOf(a).bg }">
<wd-icon :name="tileOf(a).name" size="44rpx" :color="tileOf(a).color" />
</view>
<text class="al">{{ a.label }}</text>
</view>
</view>
</view>
<view v-if="apps.length" class="sec-h">
<text class="sec-t">个人办公</text>
</view>
<view v-if="apps.length" class="card apps">
<view class="grid">
<view v-for="a in apps" :key="a.id" class="app" @click="open(a)">
<view class="icon" :style="{ background: tileOf(a).bg }">
<wd-icon :name="tileOf(a).name" size="44rpx" :color="tileOf(a).color" />
<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="sec-h">
<text class="sec-t">管理应用</text>
</view>
<view v-if="erp.length" class="card mods">
<view v-for="m in erp" :key="m.id" class="mod" @click="open(m)">
<view class="mi" :style="{ background: tileOf(m).bg }">
<wd-icon :name="tileOf(m).name" size="36rpx" :color="tileOf(m).color" />
</view>
<text class="mt">{{ m.title }}</text>
<text class="arr">›</text>
</view>
</view>
<view class="sec-h">
<view class="th">
<text class="sec-t">我的待办</text>
<text v-if="todoCount" class="badge">{{ todoCount }}</text>
</view>
<text class="more" @click="goTodo">查看全部</text>
</view>
<wd-status-tip v-if="!pending.length" image="content" tip="暂无待办" />
<view v-for="t in pending" :key="pendKey(t)" class="card todo" @click="openPending(t)">
<view class="tm">
<text class="tag">{{ t.tag || t.typeLabel || t.type || '流程审批' }}</text>
<text class="tn">{{ t.title || t.no }}</text>
<text class="ts">{{ t.submitter || '—' }}</text>
</view>
<wd-tag type="primary" plain>{{ t.btn || '去办理' }}</wd-tag>
<text class="arr">›</text>
</view>
</view>
</fy-scroll>
</view>
</template>
<script>
import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.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, tileVisual } from '../../utils/nav.js'
export default {
components: { FyHeader, FyScroll },
data() {
return { apps: [], erp: [], todos: [], pendingItems: [], shortcuts: [], todoCount: 0, announce: '', off: null }
},
computed: {
greet() {
const name = (store.me && store.me.name) || ''
const h = new Date().getHours()
const hi = h < 12 ? '上午好' : h < 18 ? '下午好' : '晚上好'
return name ? hi + ',' + name : '工作台'
},
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: tileVisual(s).name
})).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: tileVisual({ id: k.kind, title: k.title }).name
}))
},
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: {
reload() {
store.refreshWorkbench().then(() => this.pull())
},
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' }) },
tileOf(item) {
return tileVisual(item || {})
},
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: 8rpx 28rpx 48rpx; }
.hello { margin: 4rpx 4rpx 20rpx; }
.hello-sub { font-size: 26rpx; color: #6B7280; }
.stats { display: flex; gap: 16rpx; margin-bottom: 20rpx; }
.stat {
flex: 1; background: #fff; border-radius: 20rpx; padding: 22rpx 8rpx;
text-align: center; border: 1rpx solid rgba(17,24,39,0.06);
}
.sn { display: block; font-size: 36rpx; font-weight: 650; color: #1677FF; font-variant-numeric: tabular-nums; }
.sl { font-size: 24rpx; color: #6B7280; }
.notice {
background: #fff;
border: 1rpx solid rgba(17,24,39,0.06);
border-radius: 20rpx;
padding: 20rpx 24rpx;
display: flex; align-items: center;
margin-bottom: 8rpx;
}
.ntag { font-size: 22rpx; color: #EA8C00; background: #FFF6E8; border-radius: 8rpx; padding: 4rpx 10rpx; margin-right: 12rpx; }
.nlab { font-size: 26rpx; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #111827; }
.arr { color: #C4C9D4; font-size: 32rpx; margin-left: 8rpx; }
.sec-h { display: flex; justify-content: space-between; align-items: center; margin: 28rpx 4rpx 14rpx; }
.sec-t { font-size: 30rpx; font-weight: 650; color: #111827; }
.th { display: flex; align-items: center; gap: 10rpx; }
.more { font-size: 26rpx; color: #1677FF; }
.apps, .mods { padding: 12rpx 8rpx 20rpx; }
.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: 88rpx; height: 88rpx; border-radius: 24rpx; background: #E8F1FF;
display: flex; align-items: center; justify-content: center;
position: relative; margin-bottom: 10rpx;
}
.abs { position: absolute; top: -8rpx; right: -8rpx; }
.al { font-size: 24rpx; text-align: center; color: #111827; line-height: 1.35; }
.mod { display: flex; align-items: center; padding: 22rpx 16rpx; }
.mod + .mod { border-top: 1rpx solid #F0F2F5; }
.mi {
width: 64rpx; height: 64rpx; border-radius: 16rpx; background: #E8F1FF;
display: flex; align-items: center; justify-content: center;
margin-right: 16rpx;
}
.mt { flex: 1; font-size: 28rpx; }
.todo { display: flex; align-items: center; padding: 24rpx 24rpx; margin-bottom: 16rpx; }
.tm { flex: 1; min-width: 0; }
.tag { display: inline-block; font-size: 22rpx; color: #1677FF; background: #E8F1FF; border-radius: 8rpx; padding: 2rpx 10rpx; margin-bottom: 8rpx; }
.tn { display: block; font-size: 28rpx; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ts { display: block; font-size: 24rpx; color: #6B7280; margin-top: 4rpx; }
.empty { padding: 28rpx; text-align: center; color: #6B7280; font-size: 26rpx; }
.empty-card { background: #fff; border-radius: 24rpx; padding: 48rpx 32rpx; text-align: center; border: 1rpx solid rgba(17,24,39,0.06); }
.et { display: block; font-size: 28rpx; font-weight: 600; }
.es { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
.ico { padding: 8rpx 8rpx; font-size: 28rpx; color: #1677FF; }
</style>