Просмотр исходного кода

feat(mobile-h5): add OA start template list and web-view page

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
8a9d0a2f95

+ 16 - 0
mobile-h5/pages.json

@@ -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": {

+ 299 - 0
mobile-h5/pages/oa/start/index.vue

@@ -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>

+ 30 - 0
mobile-h5/pages/oa/start/webview.vue

@@ -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>

+ 15 - 0
mobile-h5/utils/oa-api.js

@@ -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())
+}