feat(mobile-h5): add OA start template list and web-view page
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
07eeb58d15
commit
8a9d0a2f95
4 files changed
+360
No files matched your search
@@ -40,6 +40,22 @@
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/oa/start/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发起审批",
|
||||
"navigationBarBackgroundColor": "#0a9f6d",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/oa/start/webview",
|
||||
"style": {
|
||||
"navigationBarTitleText": "填写审批",
|
||||
"navigationBarBackgroundColor": "#0a9f6d",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/demo/list",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="search-wrap">
|
||||
<input
|
||||
class="search"
|
||||
v-model="searchKeyword"
|
||||
confirm-type="search"
|
||||
placeholder="请输入审批单名称"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-x class="tabs" :show-scrollbar="false">
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: activeGroupId === '' }"
|
||||
@click="activeGroupId = ''"
|
||||
>
|
||||
全部
|
||||
</view>
|
||||
<view
|
||||
v-for="g in groupOptions"
|
||||
:key="g.id"
|
||||
class="tab"
|
||||
:class="{ active: activeGroupId === g.id }"
|
||||
@click="activeGroupId = g.id"
|
||||
>
|
||||
{{ g.groupName }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="hint">选择一个审批单即可发起</view>
|
||||
|
||||
<view v-if="loading" class="state">加载中…</view>
|
||||
<view v-else-if="!displayGroups.length" class="state">
|
||||
暂无可发起的审批模板
|
||||
</view>
|
||||
<view v-else class="body">
|
||||
<view v-for="sec in displayGroups" :key="sec.id" class="section">
|
||||
<view class="section-hd">
|
||||
<text class="section-name">{{ sec.name }}</text>
|
||||
<text class="section-meta">{{ sec.apps.length }} 个表单</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="row in sec.apps"
|
||||
:key="row.id"
|
||||
class="card"
|
||||
@click="onPick(row)"
|
||||
>
|
||||
<view class="card-main">
|
||||
<view class="card-icon">
|
||||
<image
|
||||
v-if="row.iconUrl"
|
||||
class="card-icon__img"
|
||||
:src="row.iconUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text v-else class="card-icon__text">{{
|
||||
(row.appName || '审').slice(0, 1)
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="card-name">{{ row.appName }}</text>
|
||||
<text class="card-desc">{{ row.description || '点击发起审批' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-btn" @click.stop="onPick(row)">发起</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fetchOaGroupList, fetchOaTemplates } from '../../../utils/oa-api.js'
|
||||
import { getToken } from '../../../utils/auth.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
templates: [],
|
||||
groupOptions: [],
|
||||
searchKeyword: '',
|
||||
activeGroupId: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayGroups() {
|
||||
const kw = (this.searchKeyword || '').trim().toLowerCase()
|
||||
let list = [...(this.templates || [])]
|
||||
if (this.activeGroupId) {
|
||||
list = list.filter((a) => a.groupId === this.activeGroupId)
|
||||
}
|
||||
if (kw) {
|
||||
list = list.filter((a) =>
|
||||
String(a.appName || '')
|
||||
.toLowerCase()
|
||||
.includes(kw)
|
||||
)
|
||||
}
|
||||
const bucket = new Map()
|
||||
for (const g of this.groupOptions) {
|
||||
bucket.set(g.id, { id: g.id, name: g.groupName, apps: [] })
|
||||
}
|
||||
const ungrouped = { id: '_ungrouped', name: '未分组', apps: [] }
|
||||
for (const app of list) {
|
||||
if (app.groupId && bucket.has(app.groupId)) {
|
||||
bucket.get(app.groupId).apps.push(app)
|
||||
} else {
|
||||
ungrouped.apps.push(app)
|
||||
}
|
||||
}
|
||||
if (this.activeGroupId) {
|
||||
const one = bucket.get(this.activeGroupId)
|
||||
return one && one.apps.length ? [one] : []
|
||||
}
|
||||
const sections = []
|
||||
for (const g of this.groupOptions) {
|
||||
const sec = bucket.get(g.id)
|
||||
if (sec && sec.apps.length) sections.push(sec)
|
||||
}
|
||||
if (ungrouped.apps.length) sections.push(ungrouped)
|
||||
return sections
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (!getToken()) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true
|
||||
try {
|
||||
const [groups, templates] = await Promise.all([
|
||||
fetchOaGroupList(),
|
||||
fetchOaTemplates()
|
||||
])
|
||||
this.groupOptions = (groups?.list || groups || []).map((g) => ({
|
||||
id: g.id,
|
||||
groupName: g.groupName || g.name || '分组'
|
||||
}))
|
||||
this.templates = templates?.list || templates || []
|
||||
} catch (e) {
|
||||
uni.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
|
||||
this.templates = []
|
||||
this.groupOptions = []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
onPick(row) {
|
||||
if (!row || !row.id) return
|
||||
uni.navigateTo({
|
||||
url: '/pages/oa/start/webview?appId=' + encodeURIComponent(row.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 24rpx;
|
||||
background: #f5f8f7;
|
||||
}
|
||||
.search-wrap {
|
||||
padding: 20rpx 24rpx 8rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.search {
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f5f8f7;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.tabs {
|
||||
white-space: nowrap;
|
||||
padding: 16rpx 16rpx 8rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
margin-right: 16rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f5f8f7;
|
||||
color: #6b7b74;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.tab.active {
|
||||
color: #fff;
|
||||
background: #0a9f6d;
|
||||
}
|
||||
.hint {
|
||||
padding: 16rpx 24rpx;
|
||||
color: #6b7b74;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
color: #6b7b74;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.body {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.section-hd {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.section-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2a26;
|
||||
}
|
||||
.section-meta {
|
||||
font-size: 22rpx;
|
||||
color: #6b7b74;
|
||||
}
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(8, 127, 87, 0.06);
|
||||
}
|
||||
.card-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.card-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background: #e9f9f2;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-icon__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.card-icon__text {
|
||||
color: #0a9f6d;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.card-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2a26;
|
||||
}
|
||||
.card:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #6b7b74;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.card-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #0a9f6d;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<web-view v-if="src" :src="src" @message="onMessage" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildOaStartUrl } from '../../../utils/oa-h5.js'
|
||||
import { getToken } from '../../../utils/auth.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { src: '' }
|
||||
},
|
||||
onLoad(query) {
|
||||
if (!getToken()) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 500)
|
||||
return
|
||||
}
|
||||
this.src = buildOaStartUrl({
|
||||
appId: query.appId,
|
||||
draftId: query.draftId
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onMessage() {
|
||||
/* 微信多在后退时触发;列表页 onShow 会刷新 */
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { get } from './request.js'
|
||||
import { getToken } from './auth.js'
|
||||
|
||||
function authHeader() {
|
||||
const token = getToken()
|
||||
return token ? { Authorization: 'Bearer ' + token } : {}
|
||||
}
|
||||
|
||||
export function fetchOaGroupList() {
|
||||
return get('/web/admin/oa-approval/groups', authHeader())
|
||||
}
|
||||
|
||||
export function fetchOaTemplates() {
|
||||
return get('/web/admin/oa-approval/templates', authHeader())
|
||||
}
|
||||
Reference in new issue
Block a user