oa_app仓库初始化

This commit is contained in:
2026-09-23 16:52:48 +08:00
commit 65c5e9f66f
82 changed files with 6490 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
<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>