这个两个ApproverSettingMain<template> <view :class="[...

作成日: 2026年1月10日

使用モデル GPT-5.2 Thinking by Chat01

質問

image.pngimage.png
image.pngimage.png

这个两个ApproverSettingMain<template>
<view :class="['app-container', theme]">
<view class="content">
<view class="overlay">
<!-- Header -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }">
<view class="header-inner">
<view class="left-box" @click="navigateBack">
<uni-icons type="left" color="#000000" size="26" />
</view>

text
<view class="center-box"> <text class="title-text">审批人添加</text> </view> <!-- UI 图右上角是“完成”,这里就是返回 --> <view class="right-box" @click="navigateBack"> <text class="done-text">完成</text> </view> </view> </view> <view class="wrap"> <view class="list-card"> <view v-for="m in approvers" :key="m.consumerId" class="item"> <image class="avatar" :src="formatAvatar(m.avatarUrl)" mode="aspectFill" /> <text class="name">{{ m.nickname || "用户名称" }}</text> <text v-if="canRemove(m)" class="action remove" @click="removeApprover(m)">移除</text> </view> <view class="add-row" @click="goAdd"> <text class="action add">添加</text> </view> </view> <view class="note"> <text class="note-text">注释:添加了审批人后该审批人可以直接同意组员的请假、补卡申请</text> </view> </view> </view> </view> </view>
</template> <script setup> import { ref, inject } from "vue"; import { onLoad, onShow } from "@dcloudio/uni-app"; import { getStatusBarHeight, getTitleBarHeight } from "@/utils/system.js"; import ajax from "@/api/ajax.js"; import { storeToRefs } from "pinia"; import { useMainStore } from "@/store/index.js"; const store = useMainStore(); const { theme } = storeToRefs(store); const statusBarHeight = ref(getStatusBarHeight()); const headerBarHeight = ref(getTitleBarHeight()); const castingTeamId = ref(""); const members = ref([]); const approvers = ref([]); let webProjectUrl = inject("webProjectUrl"); function formatAvatar(url) { if (!url) return "/static/images/default-avatar.png"; return webProjectUrl ? webProjectUrl + url : url; } function navigateBack() { uni.navigateBack(); } function getConsumerId() { const s = store || {}; const fromStore = s?.userInfo?.consumerId || s?.consumerId; if (fromStore) return String(fromStore); const id1 = uni.getStorageSync("consumerId"); if (id1) return String(id1); const u = uni.getStorageSync("userInfo"); if (u && typeof u === "object" && u.consumerId) return String(u.consumerId); if (u && typeof u === "string") { try { const obj = JSON.parse(u); if (obj?.consumerId) return String(obj.consumerId); } catch (e) {} } return ""; } function canRemove(m) { if (m.owner) return false; return true; } async function fetchMembers() { if (!castingTeamId.value) return; const res = await ajax .post({ url: "castingTeamConsumerList", data: { castingTeamId: castingTeamId.value, rowStartIdx: 0, rowCount: 200, }, method: "post", }) .catch(() => ({})); console.log(res,'里面的审批人列表') consumerArray const arr = res?.data?.consumerArray || res?.data?.castingTeamConsumerArray || res?.data?.castingTeamConsumerObject?.consumerArray || res?.data || []; const list = Array.isArray(arr) ? arr : []; members.value = list.map((x) => ({ consumerId: x.consumerId ?? x.id ?? "", nickname: x.nickname ?? "", avatarUrl: x.avatarUrl ?? "", owner: !!x.owner, manager: !!x.manager, leaveApprover: !!x.leaveApprover, })); approvers.value = members.value.filter((x) => x.leaveApprover); } async function updateApprover({ addIds = [], removeIds = [] }) { if (!castingTeamId.value) return false; // castingTeam:修改用 id(你后端文档里写的) const data = { id: castingTeamId.value }; if (addIds.length) data.leaveApproverId = addIds; if (removeIds.length) data.cancelLeaveApproverId = removeIds; const res = await ajax .post({ url: "castingTeam", data, method: "post", }) .catch(() => ({})); return !!(res?.data?.id || res?.data || res?.id); } async function removeApprover(m) { uni.showModal({ title: "提示", content: "确定移除该审批人?", success: async (r) => { if (!r.confirm) return; const ok = await updateApprover({ removeIds: [m.consumerId] }); if (ok) { uni.showToast({ title: "已移除", icon: "none" }); await fetchMembers(); } else { uni.showToast({ title: "移除失败", icon: "none" }); } }, }); } function goAdd() { uni.navigateTo({ url: "/pages/sociAlize/Coffeegroup/application/CheckinSettings/Settings/ApproverAddMember?castingTeamId=" + encodeURIComponent(castingTeamId.value), }); } onLoad(async (option) => { castingTeamId.value = option?.castingTeamId ? decodeURIComponent(option.castingTeamId) : ""; await fetchMembers(); }); onShow(async () => { // 从添加页回来刷新 if (castingTeamId.value) await fetchMembers(); }); </script> <style lang="scss" scoped> @import "@/assets/styles/global.scss"; .content { min-height: 100vh; position: relative; background: #f7f7f7; } .overlay { position: absolute; inset: 0; background: #f7f7f7; } .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .title-text { font-size: 32rpx; font-weight: 600; color: #111; } .done-text { font-size: 28rpx; color: #d5962a; font-weight: 600; } .wrap { width: 94%; margin: 18rpx auto 0; } .list-card { background: #fff; border-radius: 12rpx; overflow: hidden; } .item { height: 110rpx; padding: 0 18rpx; display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; box-sizing: border-box; } .item:last-child { border-bottom: none; } .avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #eee; margin-right: 16rpx; } .name { flex: 1; font-size: 28rpx; color: #111; } .action { font-size: 28rpx; font-weight: 600; color: #d5962a; } .add-row { height: 90rpx; display: flex; align-items: center; justify-content: flex-end; padding: 0 18rpx; border-top: 1px solid #f0f0f0; } .note { margin-top: 18rpx; } .note-text { font-size: 24rpx; color: #999; line-height: 36rpx; } </style>和ApproverAddMember页面<template>
text
<view :class="['app-container', theme]"> <view class="content"> <view class="overlay"> <!-- Header --> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <view class="left-box" @click="navigateBack"> <uni-icons type="left" color="#000000" size="26" /> </view> <view class="center-box"> <text class="title-text">添加</text> </view> </view> </view> <!-- 搜索 + 列表 --> <view class="wrap"> <view class="search-bar"> <text class="search-label">联系人</text> <input class="search-input" v-model="keyword" placeholder=" " placeholder-style="color:#C8C8C8;" confirm-type="search" /> <uni-icons type="search" size="18" color="#999" /> </view> <scroll-view class="list" scroll-y @scrolltolower="loadMore"> <view v-for="m in filteredMembers" :key="m.consumerId" class="item"> <image class="avatar" :src="formatAvatar(m.avatarUrl)" mode="aspectFill" /> <view class="info"> <text class="name">{{ m.nickname || "组备注" }}</text> </view> <text v-if="!m.leaveApprover" class="action add" @click="addApprover(m)">添加</text> <text v-else class="action added">已添加</text> </view> <view class="loadmore-wrap"> <uni-load-more :status="loadMoreStatus" /> </view> </scroll-view> </view> </view> </view> </view>
</template> <script setup> import { ref, computed, inject } from "vue"; import { onLoad } from "@dcloudio/uni-app"; import { getStatusBarHeight, getTitleBarHeight } from "@/utils/system.js"; import ajax from "@/api/ajax.js"; import { storeToRefs } from "pinia"; import { useMainStore } from "@/store/index.js"; const store = useMainStore(); const { theme } = storeToRefs(store); const statusBarHeight = ref(getStatusBarHeight()); const headerBarHeight = ref(getTitleBarHeight()); const castingTeamId = ref(""); let webProjectUrl = inject("webProjectUrl"); function formatAvatar(url) { if (!url) return "/static/images/default-avatar.png"; return webProjectUrl ? webProjectUrl + url : url; } const keyword = ref(""); const members = ref([]); const loading = ref(false); const rowStartIdx = ref(0); const rowCount = ref(30); const hasMore = ref(true); const loadMoreStatus = computed(() => { if (loading.value && members.value.length > 0) return "loading"; if (!hasMore.value) return "noMore"; return "more"; }); const filteredMembers = computed(() => { const k = (keyword.value || "").trim(); if (!k) return members.value; return members.value.filter((m) => (m.nickname || "").includes(k)); }); function navigateBack() { uni.navigateBack(); } async function fetchMembers(reset = false) { if (!castingTeamId.value) return; if (loading.value) return; if (reset) { rowStartIdx.value = 0; hasMore.value = true; members.value = []; } if (!hasMore.value) return; loading.value = true; const res = await ajax .post({ url: "castingTeamConsumerList", data: { castingTeamId: castingTeamId.value, rowStartIdx: rowStartIdx.value, rowCount: rowCount.value, }, method: "post", }) .catch(() => ({})); const arr = res?.data?.consumerArray || res?.data?.castingTeamConsumerArray || res?.data?.castingTeamConsumerObject?.consumerArray || res?.data || []; const list = Array.isArray(arr) ? arr : []; const normalized = list.map((x) => ({ consumerId: x.consumerId ?? x.id ?? "", nickname: x.nickname ?? "", avatarUrl: x.avatarUrl ?? "", owner: !!x.owner, manager: !!x.manager, leaveApprover: !!x.leaveApprover, })); members.value = members.value.concat(normalized); if (normalized.length < rowCount.value) { hasMore.value = false; } else { rowStartIdx.value += rowCount.value; } loading.value = false; } async function loadMore() { if (loading.value || !hasMore.value) return; await fetchMembers(false); } async function addApprover(m) { if (m.leaveApprover) return; let option = { id: castingTeamId.value, leaveApproverId: [m.consumerId], } console.log(option, '里面的参数来源') const res = await ajax.post({ url: "castingTeam", data: option, method: "post", }) .catch(() => ({})); console.log(res, '里面的数据来源') const ok = !!(res?.data?.id || res?.data || res?.id); if (ok) { uni.showToast({ title: "已添加", icon: "none" }); // 直接在本地标记,保持 UI 即时反馈 const idx = members.value.findIndex((x) => x.consumerId === m.consumerId); if (idx !== -1) members.value[idx].leaveApprover = true; } else { uni.showToast({ title: "添加失败", icon: "none" }); } } onLoad(async (option) => { castingTeamId.value = option?.castingTeamId ? decodeURIComponent(option.castingTeamId) : ""; await fetchMembers(true); }); </script> <style lang="scss" scoped> @import "@/assets/styles/global.scss"; .content { min-height: 100vh; position: relative; background: #f7f7f7; } .overlay { position: absolute; inset: 0; background: #f7f7f7; } .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box { position: absolute; left: 0; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; justify-content: flex-start; } .title-text { font-size: 32rpx; font-weight: 600; color: #111; } .wrap { width: 94%; margin: 18rpx auto 0; } .search-bar { height: 88rpx; background: #fff; border-radius: 12rpx; padding: 0 16rpx; box-sizing: border-box; display: flex; align-items: center; gap: 12rpx; } .search-label { font-size: 26rpx; color: #111; } .search-input { flex: 1; font-size: 26rpx; color: #111; } .list { margin-top: 16rpx; height: calc(100vh - 220rpx); background: #fff; border-radius: 12rpx; overflow: hidden; } .item { height: 110rpx; padding: 0 18rpx; display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; box-sizing: border-box; } .item:last-child { border-bottom: none; } .avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #eee; margin-right: 16rpx; } .info { flex: 1; min-width: 0; } .name { font-size: 28rpx; color: #111; } .action { font-size: 28rpx; font-weight: 600; } .action.add { color: #d5962a; } .action.added { color: #bbb; font-weight: 500; } .loadmore-wrap { padding: 10rpx 0 18rpx; } </style>在根据ui图和啊api9.castingTeam 组维护、加入、新建
入口参数

String id, 修改用
String parentId, 父组id
String title, 名称
String notice, 公告
String[] consumerIds, 加入组的用户
String ownerId,所有者id。新建时用,更换所有者时候用
String cancelOwnerId,取消管理员id,变更管理员时用
String[] managerIds,管理员id,变更管理员时用
String[] cancelManagerIds, 取消管理员id,变更管理员时用
String[] cancelLeaveApproverId, 取消审批者id, 变更审批者用
String[] leaveApproverId, 审批者id, 变更审批者用
返回值
id 组id
20.leaveAuditListt组审批人列表
入口参数
String castingTeamId, 组id
String consumerId, 审批人id
Integer rowStartIdx,起始行
Integer rowCount行数
返回值
adminArray
consumerId 用户id
nickname 群昵称
avatarUrl 头像
owner 是否是拥有者
manager 是否是管理员
leaveApprover 是否是审批者
在根据重新写两个完整的页面包括滑动不能出现白边和已经添加就显示已添加在头部加上用户名和头像

思考

Adjusting UI and fixing bounce issue in uni-app

The user wants me to rewrite code for their uni-app Vue3 project based on UI and API docs. To fix the "white border" problem, I'll adjust the background settings, use minHeight for full-page height, and ensure the scroll-view has full height with hidden overflow. Additionally, I’ll ensure that the background color and scroll behavior are consistent when bouncing on iOS. I’ll also check the main page layout and add necessary user info.

Clarifying user interface details for approver settings

They're asking about adding the username and avatar to the header, possibly on the main page or in the "ApproverAddMember" page. The screenshot shows multiple approvers and their avatars. It seems they want the current user’s info at the top of the page, but there’s no user info in the "ApproverAddMember" page header—just contacts.

Deciding on header UI for current user and approvers

It seems the user might want the current user to appear at the top of the approver list as a fixed, non-removable row, possibly marking them as the owner. The header could show their name and avatar. But the left screenshot only shows one row—maybe the current state should just show the current user's info before any approvers are added.

Clarifying header layout and user info placement

It seems they want the owner's username and avatar at the top of the main page, likely as a non-removable first approver row. The same should apply to the "Add Member" page, where the header might show the current user’s info (owner). Both pages may need user info at the top, even if the add member page already shows avatars and names in the list.

Refining approver list and page functionality

It looks like they want the main page to either show a list of current approvers or just the owner row if no others are present. The "Add Member" page should list all members with a search bar, marking those already added. For APIs, leaveAuditList seems better for the approver list, while castingTeamConsumerList works for all members with their statuses. We’ll handle the specific parameters for adding/removing members and fixing UI issues with "white edges" by modifying the page styles.

Finalizing UI adjustments and page details

They want full page designs with specific adjustments. For scoped styles, I’ll use :global(page) to apply styles outside the scoped component. I'll handle iOS overscroll with height: 100vh. For the main page header, I'll add "完成" on the right if it's a "done" button. I’ll also show user avatar and name based on the current user’s info. Finally, we’ll consider displaying "申请人" or group owner info, like seen in screenshots.

Clarifying UI layout for approvers and headers

It seems like the main page should start with a "申请人" row showing the avatar and name, with the "添加" option to add more approvers. On the add page, we need to show the current selected approvers along with a search bar. As for the header, I think they want the group owner’s info at the top, next to the title.

Clarifying layouts and API usage for approvers

It seems like the current user’s avatar and name should be shown at the top of both the main page and add page. The main page should fetch the approver list directly from the API instead of filtering the member list. We'll adjust the layout to match iOS style, with gray backgrounds and separators. In the add page, we’ll list members and show who’s been added. We'll handle infinite scroll and updates accordingly.

Designing layout and features for approvers list

I’ll use a flex layout, with the overlay set as a flex column and the header at the top. The list will take up remaining space, and I’ll ensure there are no white edges by setting explicit heights. For the UI, I’ll add the current user’s avatar/nickname at the top, followed by other approvers. I’ll implement the “增加” action at the bottom instead of the right. I’ll write final code in Chinese, keeping the structure consistent with design requirements.

Clarifying layout and functional requirements

It looks like I'll show "添加" at the bottom when there are no approvers besides the owner. The fetchApprovers function will handle the logic for parsing data and updating approval status. On the add page, I'll ensure the header displays the current selected approvers, possibly in a horizontal scroll, matching the request to add user avatars and names. This will need to be consistent across both pages.

Thought for 1m 58s

回答

下面给你把 ApproverSettingMain(审批人添加)ApproverAddMember(添加) 两个页面按你给的 UI + API(9.castingTeam20.leaveAuditListcastingTeamConsumerList)重新写成完整可用版本,并且做到:

  • 滑动不露白边(page 背景 + flex/scroll-view 高度处理)
  • ✅ 列表里 已是审批人就显示“已添加”
  • 头部展示用户名 + 头像(审批人添加页顶部固定展示“群主/拥有者”,没有就展示当前登录用户)
  • ✅ 主页面展示:拥有者(不可移除)+ 已添加审批人(可移除)+ 底部“添加”
  • ✅ 添加页:顶部展示“已添加审批人”头像/名字 + 搜索 + 成员列表 + 分页上拉加载

你只要把下面两个 .vue 文件直接替换即可(路由地址按你项目路径保留了你原来的写法)。


1) ApproverSettingMain.vue(审批人添加)

vue
<template> <view :class="['page', theme]"> <!-- 顶部栏 --> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <view class="left" @click="navigateBack"> <uni-icons type="left" color="#000" size="26" /> </view> <view class="center"> <text class="title">审批人添加</text> </view> <view class="right" @click="navigateBack"> <text class="done">完成</text> </view> </view> </view> <!-- 主体滚动区:用 scroll-view + flex:1,避免页面滚动露白 --> <scroll-view class="scroll" scroll-y> <view class="wrap"> <view class="list"> <!-- 头部:显示拥有者(优先) 或 当前用户 --> <view v-if="headerUser" class="item"> <image class="avatar" :src="formatAvatar(headerUser.avatarUrl)" mode="aspectFill" /> <text class="name">{{ headerUser.nickname || "用户名称" }}</text> </view> <!-- 审批人列表(不含 owner) --> <view v-for="m in approverList" :key="m.consumerId" class="item"> <image class="avatar" :src="formatAvatar(m.avatarUrl)" mode="aspectFill" /> <text class="name">{{ m.nickname || "用户名称" }}</text> <text v-if="canRemove(m)" class="action remove" @click="removeApprover(m)">移除</text> </view> <!-- 添加入口 --> <view class="add-row" @click="goAdd"> <text class="action add">添加</text> </view> </view> <view class="note"> <text class="note-text"> 注释:添加了审批人后该审批人可以直接同意组员的请假、补卡申请 </text> </view> </view> </scroll-view> </view> </template> <script setup> import { ref, inject, computed } from "vue"; import { onLoad, onShow } from "@dcloudio/uni-app"; import { getStatusBarHeight, getTitleBarHeight } from "@/utils/system.js"; import ajax from "@/api/ajax.js"; import { storeToRefs } from "pinia"; import { useMainStore } from "@/store/index.js"; const store = useMainStore(); const { theme } = storeToRefs(store); const statusBarHeight = ref(getStatusBarHeight()); const headerBarHeight = ref(getTitleBarHeight()); const castingTeamId = ref(""); let webProjectUrl = inject("webProjectUrl"); // 拥有者/头部用户 const ownerUser = ref(null); // 审批人列表(含 owner 时也可能返回 owner) const approversRaw = ref([]); /** 头像处理 */ function formatAvatar(url) { if (!url) return "/static/images/default-avatar.png"; return webProjectUrl ? webProjectUrl + url : url; } function navigateBack() { uni.navigateBack(); } /** 当前登录用户 consumerId(用于 leaveAuditList 入参 consumerId) */ function getConsumerId() { const s = store || {}; const fromStore = s?.userInfo?.consumerId || s?.consumerId; if (fromStore) return String(fromStore); const id1 = uni.getStorageSync("consumerId"); if (id1) return String(id1); const u = uni.getStorageSync("userInfo"); if (u && typeof u === "object" && u.consumerId) return String(u.consumerId); if (u && typeof u === "string") { try { const obj = JSON.parse(u); if (obj?.consumerId) return String(obj.consumerId); } catch (e) {} } return ""; } /** 头部展示用户:优先 owner,其次当前用户 */ const headerUser = computed(() => { if (ownerUser.value) return ownerUser.value; const u = store?.userInfo || {}; if (u?.consumerId || u?.nickname || u?.avatarUrl) { return { consumerId: u.consumerId || "", nickname: u.nickname || u.name || "用户名称", avatarUrl: u.avatarUrl || "", owner: true, leaveApprover: true, }; } return null; }); /** 只展示“可移除审批人”:过滤掉 owner */ const approverList = computed(() => { const list = approversRaw.value || []; return list.filter((x) => x.leaveApprover && !x.owner); }); /** owner 不可移除 */ function canRemove(m) { return !m.owner; } /** 20.leaveAuditList:组审批人列表 */ async function fetchApprovers() { if (!castingTeamId.value) return; const res = await ajax .post({ url: "leaveAuditList", data: { castingTeamId: castingTeamId.value, consumerId: getConsumerId(), rowStartIdx: 0, rowCount: 200, }, method: "post", }) .catch(() => ({})); const arr = res?.data?.adminArray || res?.data?.consumerArray || res?.data?.castingTeamConsumerArray || res?.data?.castingTeamConsumerObject?.consumerArray || res?.data || []; const list = Array.isArray(arr) ? arr : []; const normalized = list.map((x) => ({ consumerId: String(x.consumerId ?? x.id ?? ""), nickname: x.nickname ?? "", avatarUrl: x.avatarUrl ?? "", owner: !!x.owner, manager: !!x.manager, leaveApprover: !!x.leaveApprover, })); // owner 优先取第一个 owner ownerUser.value = normalized.find((x) => x.owner) || null; approversRaw.value = normalized.filter((x) => x.leaveApprover); } /** 9.castingTeam:变更审批人(移除) */ async function updateApprover({ addIds = [], removeIds = [] }) { if (!castingTeamId.value) return false; const data = { id: castingTeamId.value }; if (addIds.length) data.leaveApproverId = addIds; if (removeIds.length) data.cancelLeaveApproverId = removeIds; const res = await ajax .post({ url: "castingTeam", data, method: "post", }) .catch(() => ({})); return !!(res?.data?.id || res?.data || res?.id); } async function removeApprover(m) { uni.showModal({ title: "提示", content: "确定移除该审批人?", success: async (r) => { if (!r.confirm) return; const ok = await updateApprover({ removeIds: [m.consumerId] }); if (ok) { uni.showToast({ title: "已移除", icon: "none" }); await fetchApprovers(); } else { uni.showToast({ title: "移除失败", icon: "none" }); } }, }); } function goAdd() { uni.navigateTo({ url: "/pages/sociAlize/Coffeegroup/application/CheckinSettings/Settings/ApproverAddMember?castingTeamId=" + encodeURIComponent(castingTeamId.value), }); } onLoad(async (option) => { castingTeamId.value = option?.castingTeamId ? decodeURIComponent(option.castingTeamId) : ""; await fetchApprovers(); }); onShow(async () => { if (castingTeamId.value) await fetchApprovers(); }); </script> <style lang="scss" scoped> /* 关键:防止 iOS 下滑动露白 */ :global(page) { background-color: #f7f7f7; } :global(body) { background-color: #f7f7f7; } .page { min-height: 100vh; background: #f7f7f7; display: flex; flex-direction: column; overflow: hidden; } .header { background: #f7f7f7; padding-left: 3%; padding-right: 3%; } .header-inner { position: relative; height: 100%; display: flex; align-items: center; justify-content: center; } .left, .right { position: absolute; top: 0; bottom: 0; width: 140rpx; display: flex; align-items: center; } .left { left: 0; justify-content: flex-start; } .right { right: 0; justify-content: flex-end; } .title { font-size: 32rpx; font-weight: 600; color: #111; } .done { font-size: 28rpx; font-weight: 600; color: #d5962a; } .scroll { flex: 1; height: 0; /* flex + scroll-view 必须配 height:0 */ } .wrap { width: 94%; margin: 18rpx auto 24rpx; padding-bottom: env(safe-area-inset-bottom); } .list { background: #fff; border-radius: 12rpx; overflow: hidden; } .item { height: 110rpx; padding: 0 18rpx; display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; box-sizing: border-box; } .item:last-child { border-bottom: none; } .avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #eee; margin-right: 16rpx; } .name { flex: 1; font-size: 28rpx; color: #111; } .action { font-size: 28rpx; font-weight: 600; } .remove { color: #d5962a; } .add-row { height: 90rpx; display: flex; align-items: center; justify-content: flex-end; padding: 0 18rpx; border-top: 1px solid #f0f0f0; } .add { color: #d5962a; } .note { margin-top: 18rpx; } .note-text { font-size: 24rpx; color: #999; line-height: 36rpx; } </style>

2) ApproverAddMember.vue(添加成员为审批人)

vue
<template> <view :class="['page', theme]"> <!-- 顶部栏 --> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <view class="left" @click="navigateBack"> <uni-icons type="left" color="#000" size="26" /> </view> <view class="center"> <text class="title">添加</text> </view> </view> </view> <view class="body"> <!-- 头部:已添加审批人(头像+名字) --> <view v-if="selectedApprovers.length" class="selected-card"> <view class="selected-title">已添加</view> <scroll-view class="selected-scroll" scroll-x> <view class="selected-item" v-for="p in selectedApprovers" :key="p.consumerId"> <image class="selected-avatar" :src="formatAvatar(p.avatarUrl)" mode="aspectFill" /> <text class="selected-name">{{ p.nickname || "用户名称" }}</text> </view> </scroll-view> </view> <!-- 搜索 --> <view class="search-bar"> <text class="search-label">联系人</text> <input class="search-input" v-model="keyword" placeholder=" " placeholder-style="color:#C8C8C8;" confirm-type="search" /> <uni-icons type="search" size="18" color="#999" /> </view> <!-- 列表 --> <scroll-view class="list" scroll-y @scrolltolower="loadMore"> <view v-for="m in filteredMembers" :key="m.consumerId" class="item"> <image class="avatar" :src="formatAvatar(m.avatarUrl)" mode="aspectFill" /> <view class="info"> <text class="name">{{ m.nickname || "组备注" }}</text> </view> <text v-if="!m.leaveApprover" class="action add" @click="addApprover(m)">添加</text> <text v-else class="action added">已添加</text> </view> <view class="loadmore-wrap"> <uni-load-more :status="loadMoreStatus" /> </view> </scroll-view> </view> </view> </template> <script setup> import { ref, computed, inject } from "vue"; import { onLoad } from "@dcloudio/uni-app"; import { getStatusBarHeight, getTitleBarHeight } from "@/utils/system.js"; import ajax from "@/api/ajax.js"; import { storeToRefs } from "pinia"; import { useMainStore } from "@/store/index.js"; const store = useMainStore(); const { theme } = storeToRefs(store); const statusBarHeight = ref(getStatusBarHeight()); const headerBarHeight = ref(getTitleBarHeight()); const castingTeamId = ref(""); let webProjectUrl = inject("webProjectUrl"); function formatAvatar(url) { if (!url) return "/static/images/default-avatar.png"; return webProjectUrl ? webProjectUrl + url : url; } function navigateBack() { uni.navigateBack(); } /** 顶部已添加审批人 */ const selectedApprovers = ref([]); /** 搜索关键字 */ const keyword = ref(""); /** 成员列表分页 */ const members = ref([]); const loading = ref(false); const rowStartIdx = ref(0); const rowCount = ref(30); const hasMore = ref(true); const loadMoreStatus = computed(() => { if (loading.value && members.value.length > 0) return "loading"; if (!hasMore.value) return "noMore"; return "more"; }); const filteredMembers = computed(() => { const k = (keyword.value || "").trim(); if (!k) return members.value; return members.value.filter((m) => (m.nickname || "").includes(k)); }); function getConsumerId() { const s = store || {}; const fromStore = s?.userInfo?.consumerId || s?.consumerId; if (fromStore) return String(fromStore); const id1 = uni.getStorageSync("consumerId"); if (id1) return String(id1); const u = uni.getStorageSync("userInfo"); if (u && typeof u === "object" && u.consumerId) return String(u.consumerId); if (u && typeof u === "string") { try { const obj = JSON.parse(u); if (obj?.consumerId) return String(obj.consumerId); } catch (e) {} } return ""; } /** 20.leaveAuditList:取已添加审批人(用于头部展示) */ async function fetchSelectedApprovers() { if (!castingTeamId.value) return; const res = await ajax .post({ url: "leaveAuditList", data: { castingTeamId: castingTeamId.value, consumerId: getConsumerId(), rowStartIdx: 0, rowCount: 200, }, method: "post", }) .catch(() => ({})); const arr = res?.data?.adminArray || res?.data?.consumerArray || res?.data || []; const list = Array.isArray(arr) ? arr : []; selectedApprovers.value = list .map((x) => ({ consumerId: String(x.consumerId ?? x.id ?? ""), nickname: x.nickname ?? "", avatarUrl: x.avatarUrl ?? "", owner: !!x.owner, leaveApprover: !!x.leaveApprover, })) .filter((x) => x.leaveApprover); } /** castingTeamConsumerList:全成员列表(带 leaveApprover 标记) */ async function fetchMembers(reset = false) { if (!castingTeamId.value) return; if (loading.value) return; if (reset) { rowStartIdx.value = 0; hasMore.value = true; members.value = []; } if (!hasMore.value) return; loading.value = true; const res = await ajax .post({ url: "castingTeamConsumerList", data: { castingTeamId: castingTeamId.value, rowStartIdx: rowStartIdx.value, rowCount: rowCount.value, }, method: "post", }) .catch(() => ({})); const arr = res?.data?.consumerArray || res?.data?.castingTeamConsumerArray || res?.data?.castingTeamConsumerObject?.consumerArray || res?.data || []; const list = Array.isArray(arr) ? arr : []; const normalized = list.map((x) => ({ consumerId: String(x.consumerId ?? x.id ?? ""), nickname: x.nickname ?? "", avatarUrl: x.avatarUrl ?? "", owner: !!x.owner, manager: !!x.manager, leaveApprover: !!x.leaveApprover, })); members.value = members.value.concat(normalized); if (normalized.length < rowCount.value) { hasMore.value = false; } else { rowStartIdx.value += rowCount.value; } loading.value = false; } async function loadMore() { if (loading.value || !hasMore.value) return; await fetchMembers(false); } /** 9.castingTeam:添加审批人 */ async function addApprover(m) { if (m.leaveApprover) return; const res = await ajax .post({ url: "castingTeam", data: { id: castingTeamId.value, leaveApproverId: [m.consumerId], }, method: "post", }) .catch(() => ({})); const ok = !!(res?.data?.id || res?.data || res?.id); if (!ok) { uni.showToast({ title: "添加失败", icon: "none" }); return; } uni.showToast({ title: "已添加", icon: "none" }); // 1) 本地标记“已添加” const idx = members.value.findIndex((x) => x.consumerId === m.consumerId); if (idx !== -1) members.value[idx].leaveApprover = true; // 2) 顶部已添加刷新(更稳) await fetchSelectedApprovers(); } onLoad(async (option) => { castingTeamId.value = option?.castingTeamId ? decodeURIComponent(option.castingTeamId) : ""; // 先拉已添加,再拉成员列表 await fetchSelectedApprovers(); await fetchMembers(true); }); </script> <style lang="scss" scoped> /* 关键:防止 iOS 下滑动露白 */ :global(page) { background-color: #f7f7f7; } :global(body) { background-color: #f7f7f7; } .page { min-height: 100vh; background: #f7f7f7; display: flex; flex-direction: column; overflow: hidden; } .header { background: #f7f7f7; padding-left: 3%; padding-right: 3%; } .header-inner { position: relative; height: 100%; display: flex; align-items: center; justify-content: center; } .left { position: absolute; left: 0; top: 0; bottom: 0; width: 140rpx; display: flex; align-items: center; justify-content: flex-start; } .title { font-size: 32rpx; font-weight: 600; color: #111; } .body { flex: 1; height: 0; width: 94%; margin: 18rpx auto 0; display: flex; flex-direction: column; padding-bottom: env(safe-area-inset-bottom); } /* 顶部已添加审批人 */ .selected-card { background: #fff; border-radius: 12rpx; padding: 16rpx 16rpx 10rpx; margin-bottom: 16rpx; overflow: hidden; } .selected-title { font-size: 24rpx; color: #999; margin-bottom: 10rpx; } .selected-scroll { white-space: nowrap; } .selected-item { display: inline-flex; flex-direction: column; align-items: center; margin-right: 18rpx; width: 110rpx; } .selected-avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #eee; } .selected-name { margin-top: 8rpx; font-size: 22rpx; color: #111; width: 110rpx; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* 搜索栏 */ .search-bar { height: 88rpx; background: #fff; border-radius: 12rpx; padding: 0 16rpx; box-sizing: border-box; display: flex; align-items: center; gap: 12rpx; } .search-label { font-size: 26rpx; color: #111; } .search-input { flex: 1; font-size: 26rpx; color: #111; } /* 列表 */ .list { margin-top: 16rpx; flex: 1; height: 0; /* 关键:避免滚动露白 */ background: #fff; border-radius: 12rpx; overflow: hidden; } .item { height: 110rpx; padding: 0 18rpx; display: flex; align-items: center; border-bottom: 1px solid #f0f0f0; box-sizing: border-box; } .item:last-child { border-bottom: none; } .avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #eee; margin-right: 16rpx; } .info { flex: 1; min-width: 0; } .name { font-size: 28rpx; color: #111; } .action { font-size: 28rpx; font-weight: 600; } .action.add { color: #d5962a; } .action.added { color: #bbb; font-weight: 500; } .loadmore-wrap { padding: 10rpx 0 18rpx; } </style>

如果你这边 leaveAuditList 实际返回字段不是 adminArray(比如是 consumerArray 或别的),我在代码里已经做了多种 fallback;你只要把控制台打印一下真实结构,我还能帮你把解析再收敛得更干净。

このQ&Aを共有