263 lines
10 KiB
Vue
263 lines
10 KiB
Vue
<template>
|
||
<view class="page">
|
||
<fy-header :title="greet">
|
||
<template #right>
|
||
<text class="ico" @click="goSearch">搜索</text>
|
||
</template>
|
||
</fy-header>
|
||
|
||
<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"><text class="mark">{{ mark(a.label) }}</text></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">
|
||
<text class="mark">{{ mark(a.title) }}</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="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"><text>{{ mark(m.title) }}</text></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>
|
||
</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: {
|
||
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: 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' }) },
|
||
mark(s) { return String(s || '应').replace(/\s/g, '').slice(0, 1) },
|
||
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: #F3F6FB;
|
||
display: flex; align-items: center; justify-content: center;
|
||
position: relative; margin-bottom: 10rpx;
|
||
}
|
||
.mark { font-size: 30rpx; font-weight: 650; color: #1677FF; }
|
||
.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: #F3F6FB;
|
||
color: #1677FF; display: flex; align-items: center; justify-content: center;
|
||
font-weight: 650; 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>
|