This commit is contained in:
2026-09-29 15:17:13 +08:00
commit 69e6ed8c59
113 changed files with 36564 additions and 0 deletions
@@ -0,0 +1,344 @@
<template>
<view>
<view class="record">
<view>
<view class="record-meta" @click="goDetail" v-if="item.taskTitle">任务:{{ item.taskTitle }}</view>
<view class="record-meta" @click="goDetail" v-if="item.chainName && item.chainName !== '-'">连锁:{{ item.chainName }}</view>
<view class="record-meta" @click="goDetail" v-if="item.merchantName">门店:{{ item.merchantName }}</view>
<view class="record-meta" @click="goDetail">提交人:{{ item.owner }} · {{ item.submitTime }}</view>
<view v-if="item.imageList && item.imageList.length" class="record-media">
<view v-if="item.imageList && item.imageList.length" class="media-images">
<image
v-for="(img, idx) in item.imageList"
:key="idx"
class="media-image"
:src="resolveMediaUrl(img)"
mode="aspectFill"
@click="emitPreview(resolveMediaUrl(img), buildPreviewList(item.imageList))"
/>
</view>
</view>
</view>
<view class="record-actions" v-if="item.status === 1">
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="$emit('approve', item)">通过</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p danger-chip" @tap="$emit('reject', item)">驳回</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="openCopyPopup">抄送</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="goDetail">查看详情</view>
</view>
</view>
<view class="record-actions" v-if="item.status === 3">
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="openCopyPopup">抄送</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="goDetail">查看详情</view>
</view>
</view>
</view>
<view v-if="showCopyPopup" class="copy-popup-mask" @tap="closeCopyPopup" @touchmove.stop.prevent></view>
<view v-if="showCopyPopup" class="copy-popup card" @tap.stop>
<view class="copy-popup-head">
<view class="section-title">抄送</view>
<view class="close-btn" @tap="closeCopyPopup">关闭</view>
</view>
<view class="copy-search-row">
<input
v-model="copyKeyword"
class="copy-search-input"
type="text"
placeholder="输入姓名/手机号搜索用户"
@confirm="searchCopyUsers"
/>
<view class="copy-search-btn" @tap="searchCopyUsers">搜索</view>
</view>
<view v-if="copySearching" class="copy-loading">搜索中...</view>
<scroll-view v-else class="copy-user-list" scroll-y>
<view
v-for="user in copyUsers"
:key="`copy-user-${user.id}`"
:class="['copy-user-item', selectedCopyUserId === user.id ? 'copy-user-item-active' : '']"
@tap="selectedCopyUserId = user.id"
>
<text class="copy-user-name">{{ user.label }}</text>
<text class="copy-user-phone">{{ user.phone || '' }}</text>
</view>
<view v-if="copyUsers.length === 0" class="copy-empty">暂无用户</view>
</scroll-view>
<view class="primary-btn copy-submit" @tap="submitCopy">确认抄送</view>
</view>
</view>
</template>
<script>
import {userSearchReq} from "@/api/user";
import {copyUserTaskReq} from "@/api/task";
export default {
name: 'AuditMovementItem',
props: {
item: {
type: Object,
required: true
}
},
data() {
return {
showCopyPopup: false,
copyKeyword: '',
copyUsers: [],
copySearching: false,
selectedCopyUserId: ''
}
},
methods: {
emitPreview(url, list) {
this.$emit('preview', url, list)
},
resolveMediaUrl(media) {
if (!media) return ''
if (typeof media === 'string') return media
return media.url || media.path || media.image || ''
},
buildPreviewList(list) {
const items = Array.isArray(list) ? list : []
return items.map(item => this.resolveMediaUrl(item)).filter(Boolean)
},
goDetail() {
const taskId = this.item.taskId
if (taskId === '' || taskId === null || taskId === undefined) {
uni.showToast({
title: '任务ID缺失',
icon: 'none'
})
return
}
const title = this.item.merchantName || this.item.taskTitle || ''
const reward = this.item.taskReward !== undefined && this.item.taskReward !== null ? this.item.taskReward : ''
const target = this.item.taskTarget || ''
let url = `/pages/task/detail?id=${taskId}`
url += `&fromAudit=1`
if (this.item.id !== undefined && this.item.id !== null && this.item.id !== '') {
url += `&auditId=${encodeURIComponent(String(this.item.id))}`
}
if (this.item.taskId !== undefined && this.item.taskId !== null && this.item.taskId !== '') {
url += `&auditTaskId=${encodeURIComponent(String(this.item.taskId))}`
}
if (this.item.userId !== undefined && this.item.userId !== null && this.item.userId !== '') {
url += `&auditUserId=${encodeURIComponent(String(this.item.userId))}`
}
if (this.item.status !== undefined && this.item.status !== null && this.item.status !== '') {
url += `&auditStatus=${encodeURIComponent(String(this.item.status))}`
}
if (title) url += `&title=${encodeURIComponent(title)}`
if (reward !== '') url += `&reward=${encodeURIComponent(String(reward))}`
if (target) url += `&target=${encodeURIComponent(target)}`
uni.navigateTo({url})
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result']
for (let i = 0; i < keys.length; i++) {
if (Array.isArray(data[keys[i]])) return data[keys[i]]
}
return []
},
normalizeUser(item) {
const id = item.id ?? item.userId ?? item.uid
const nickname = item.nickname || item.nickName || ''
const realName = item.realName || item.real_name || item.name || ''
const phone = item.phone || item.mobile || item.tel || ''
const label = realName || nickname || phone || `用户${id || ''}`
return {
id,
label,
phone
}
},
openCopyPopup() {
this.copyKeyword = ''
this.copyUsers = []
this.selectedCopyUserId = ''
this.showCopyPopup = true
},
closeCopyPopup() {
this.showCopyPopup = false
},
async searchCopyUsers() {
const keyword = (this.copyKeyword || '').trim()
if (!keyword) {
uni.showToast({
title: '请输入keyword',
icon: 'none'
})
return
}
this.copySearching = true
try {
const {data} = await userSearchReq({
keywords: keyword
})
const list = this.unwrapList(data)
this.copyUsers = list
.map(item => this.normalizeUser(item))
.filter(item => item.id !== undefined && item.id !== null && String(item.id).trim() !== '')
this.selectedCopyUserId = this.copyUsers[0] ? this.copyUsers[0].id : ''
} catch (error) {
this.copyUsers = []
uni.showToast({
title: '搜索失败',
icon: 'none'
})
} finally {
this.copySearching = false
}
},
async submitCopy() {
const targetUserId = this.selectedCopyUserId
if (targetUserId === '' || targetUserId === null || targetUserId === undefined) {
uni.showToast({
title: '请选择用户',
icon: 'none'
})
return
}
try {
await copyUserTaskReq({
id: this.item.id,
taskId: this.item.taskId,
sourceUserId: this.item.userId,
userId: targetUserId,
isCopy: 1
})
uni.showToast({
title: '抄送成功',
icon: 'success'
})
this.closeCopyPopup()
this.$emit('copied')
} catch (error) {
uni.showToast({
title: '抄送失败',
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
@import './record.scss';
.copy-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 30;
}
.copy-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: 88%;
max-height: 70vh;
overflow: auto;
z-index: 40;
}
.copy-popup-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.close-btn {
color: var(--text-sub);
font-size: 24rpx;
}
.copy-search-row {
display: flex;
gap: 10rpx;
align-items: center;
}
.copy-search-input {
flex: 1;
height: 70rpx;
line-height: 70rpx;
border-radius: 12rpx;
background: #f3f4f6;
padding: 0 16rpx;
font-size: 26rpx;
}
.copy-search-btn {
min-width: 110rpx;
height: 70rpx;
border-radius: 12rpx;
background: var(--brand-primary);
color: #ffffff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.copy-loading,
.copy-empty {
margin-top: 14rpx;
font-size: 24rpx;
color: var(--text-sub);
}
.copy-user-list {
margin-top: 14rpx;
max-height: 420rpx;
}
.copy-user-item {
padding: 12rpx;
border-radius: 12rpx;
background: #f9fafb;
margin-bottom: 10rpx;
border: 1rpx solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.copy-user-item-active {
border-color: var(--brand-primary);
background: #ecfdf5;
}
.copy-user-name {
font-size: 26rpx;
color: var(--text-main);
}
.copy-user-phone {
font-size: 22rpx;
color: var(--text-sub);
}
.copy-submit {
margin-top: 14rpx;
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,348 @@
<template>
<view>
<view class="record">
<view>
<view class="record-meta" @click="goDetail" v-if="item.taskTitle">任务:{{ item.taskTitle }}</view>
<view class="record-meta" @click="goDetail" v-if="item.chainName && item.chainName !== '-'">连锁:{{ item.chainName }}</view>
<view class="record-meta" @click="goDetail" v-if="item.merchantName">门店:{{ item.merchantName }}</view>
<view class="record-meta" @click="goDetail">提交人:{{ item.owner }} · {{ item.submitTime }}</view>
<view v-if="item.imageList && item.imageList.length" class="record-media">
<view v-if="item.imageList && item.imageList.length" class="media-images">
<image
v-for="(img, idx) in item.imageList"
:key="idx"
class="media-image"
:src="resolveMediaUrl(img)"
mode="aspectFill"
@click="emitPreview(resolveMediaUrl(img), buildPreviewList(item.imageList))"
/>
</view>
</view>
</view>
<view class="record-actions" v-if="item.status === 1">
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="$emit('approve', item)">通过</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p danger-chip" @tap="$emit('reject', item)">驳回</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="openCopyPopup">抄送</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="goDetail">查看详情</view>
</view>
</view>
<view class="record-actions" v-if="item.status === 3">
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="openCopyPopup">抄送</view>
</view>
<view class="flex-1 mx-15">
<view class="chip w-90p" @tap="goDetail">查看详情</view>
</view>
</view>
</view>
<view v-if="showCopyPopup" class="copy-popup-mask" @tap="closeCopyPopup" @touchmove.stop.prevent></view>
<view v-if="showCopyPopup" class="copy-popup card" @tap.stop>
<view class="copy-popup-head">
<view class="section-title">抄送</view>
<view class="close-btn" @tap="closeCopyPopup">关闭</view>
</view>
<view class="copy-search-row">
<input
v-model="copyKeyword"
class="copy-search-input"
type="text"
placeholder="输入姓名/手机号搜索用户"
@confirm="searchCopyUsers"
/>
<view class="copy-search-btn" @tap="searchCopyUsers">搜索</view>
</view>
<view v-if="copySearching" class="copy-loading">搜索中...</view>
<scroll-view v-else class="copy-user-list" scroll-y>
<view
v-for="user in copyUsers"
:key="`copy-user-${user.id}`"
:class="['copy-user-item', selectedCopyUserId === user.id ? 'copy-user-item-active' : '']"
@tap="selectedCopyUserId = user.id"
>
<text class="copy-user-name">{{ user.label }}</text>
<text class="copy-user-phone">{{ user.phone || '' }}</text>
</view>
<view v-if="copyUsers.length === 0" class="copy-empty">暂无用户</view>
</scroll-view>
<view class="primary-btn copy-submit" @tap="submitCopy">确认抄送</view>
</view>
</view>
</template>
<script>
import {userSearchReq} from "@/api/user";
import {copyUserTaskReq} from "@/api/task";
export default {
name: 'AuditStoreVisitItem',
props: {
item: {
type: Object,
required: true
}
},
data() {
return {
showCopyPopup: false,
copyKeyword: '',
copyUsers: [],
copySearching: false,
selectedCopyUserId: ''
}
},
methods: {
emitPreview(url, list) {
this.$emit('preview', url, list)
},
resolveMediaUrl(media) {
if (!media) return ''
if (typeof media === 'string') return media
return media.url || media.path || media.image || ''
},
buildPreviewList(list) {
const items = Array.isArray(list) ? list : []
return items.map(item => this.resolveMediaUrl(item)).filter(Boolean)
},
goDetail() {
const taskId = this.item.taskId
if (taskId === '' || taskId === null || taskId === undefined) {
uni.showToast({
title: '任务ID缺失',
icon: 'none'
})
return
}
const title = this.item.merchantName || this.item.taskTitle || ''
const reward = this.item.taskReward !== undefined && this.item.taskReward !== null ? this.item.taskReward : ''
const target = this.item.taskTarget || ''
let url = `/pages/task/detail?id=${taskId}`
url += `&fromAudit=1`
if (this.item.id !== undefined && this.item.id !== null && this.item.id !== '') {
url += `&auditId=${encodeURIComponent(String(this.item.id))}`
}
if (this.item.taskId !== undefined && this.item.taskId !== null && this.item.taskId !== '') {
url += `&auditTaskId=${encodeURIComponent(String(this.item.taskId))}`
}
if (this.item.userId !== undefined && this.item.userId !== null && this.item.userId !== '') {
url += `&auditUserId=${encodeURIComponent(String(this.item.userId))}`
}
if (this.item.status !== undefined && this.item.status !== null && this.item.status !== '') {
url += `&auditStatus=${encodeURIComponent(String(this.item.status))}`
}
if (title) url += `&title=${encodeURIComponent(title)}`
if (reward !== '') url += `&reward=${encodeURIComponent(String(reward))}`
if (target) url += `&target=${encodeURIComponent(target)}`
uni.navigateTo({url})
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result']
for (let i = 0; i < keys.length; i++) {
if (Array.isArray(data[keys[i]])) return data[keys[i]]
}
return []
},
normalizeUser(item) {
const id = item.id ?? item.userId ?? item.uid
const nickname = item.nickname || item.nickName || ''
const realName = item.realName || item.real_name || item.name || ''
const phone = item.phone || item.mobile || item.tel || ''
const label = realName || nickname || phone || `用户${id || ''}`
return {
id,
label,
phone
}
},
openCopyPopup() {
this.copyKeyword = ''
this.copyUsers = []
this.selectedCopyUserId = ''
this.showCopyPopup = true
},
closeCopyPopup() {
this.showCopyPopup = false
},
async searchCopyUsers() {
const keyword = (this.copyKeyword || '').trim()
if (!keyword) {
uni.showToast({
title: '请输入keyword',
icon: 'none'
})
return
}
this.copySearching = true
try {
const {data} = await userSearchReq({
keywords: keyword
})
const list = this.unwrapList(data)
this.copyUsers = list
.map(item => this.normalizeUser(item))
.filter(item => item.id !== undefined && item.id !== null && String(item.id).trim() !== '')
this.selectedCopyUserId = this.copyUsers[0] ? this.copyUsers[0].id : ''
} catch (error) {
this.copyUsers = []
uni.showToast({
title: '搜索失败',
icon: 'none'
})
} finally {
this.copySearching = false
}
},
async submitCopy() {
const targetUserId = this.selectedCopyUserId
if (targetUserId === '' || targetUserId === null || targetUserId === undefined) {
uni.showToast({
title: '请选择用户',
icon: 'none'
})
return
}
try {
await copyUserTaskReq({
id: this.item.id,
taskId: this.item.taskId,
sourceUserId: this.item.userId,
userId: targetUserId,
isCopy: 1
})
uni.showToast({
title: '抄送成功',
icon: 'success'
})
this.closeCopyPopup()
this.$emit('copied')
} catch (error) {
uni.showToast({
title: '抄送失败',
icon: 'none'
})
}
}
}
}
</script>
<style lang="scss" scoped>
@import './record.scss';
.copy-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 30;
}
.copy-popup {
position: fixed;
left: 50%;
bottom: 40rpx;
transform: translateX(-50%);
width: 88%;
max-height: 70vh;
overflow: auto;
z-index: 40;
}
.copy-popup-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.close-btn {
color: #6b7280;
font-size: 24rpx;
}
.copy-search-row {
display: flex;
gap: 10rpx;
align-items: center;
}
.copy-search-input {
flex: 1;
height: 70rpx;
line-height: 70rpx;
border-radius: 12rpx;
background: #f3f4f6;
padding: 0 16rpx;
font-size: 26rpx;
}
.copy-search-btn {
min-width: 110rpx;
height: 70rpx;
border-radius: 12rpx;
background: #2fc6a7;
color: #ffffff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.copy-loading,
.copy-empty {
margin-top: 14rpx;
font-size: 24rpx;
color: #6b7280;
}
.copy-user-list {
margin-top: 14rpx;
max-height: 420rpx;
}
.copy-user-item {
padding: 12rpx;
border-radius: 12rpx;
background: #f9fafb;
margin-bottom: 10rpx;
border: 1rpx solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.copy-user-item-active {
border-color: #2fc6a7;
background: #ecfdf5;
}
.copy-user-name {
font-size: 26rpx;
color: #111827;
}
.copy-user-phone {
font-size: 22rpx;
color: #6b7280;
}
.copy-submit {
margin-top: 16rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>
+220
View File
@@ -0,0 +1,220 @@
.record {
//border-bottom: 15rpx solid #f5f5f5;
//border-radius: 999rpx;
padding: 12rpx 20rpx;
border-radius: 40rpx;
background-color: white;
margin: 15rpx 0;
}
.record-title {
font-size: 30rpx;
color: var(--text-main);
}
.record-meta {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 6rpx;
}
.record-media {
margin-top: 10rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.record-extra {
margin-top: 10rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.record-section {
margin-top: 10rpx;
}
.record-section-title {
font-size: 26rpx;
color: var(--text-main);
font-weight: 600;
margin-bottom: 6rpx;
}
.clock-list {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
}
.clock-item {
width: 200rpx;
}
.clock-image {
width: 200rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
}
.clock-meta {
margin-top: 6rpx;
font-size: 22rpx;
color: var(--text-sub);
}
.clock-label {
margin-right: 6rpx;
color: var(--text-main);
font-weight: 600;
}
.sales-audit-group {
background: #f7f9fc;
border-radius: 12rpx;
padding: 12rpx;
margin-bottom: 8rpx;
}
.sales-audit-group:last-of-type {
margin-bottom: 0;
}
.sales-audit-title {
font-size: 24rpx;
color: var(--text-main);
font-weight: 600;
margin-bottom: 6rpx;
}
.sales-audit-item {
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: var(--text-main);
padding: 4rpx 0;
}
.sales-audit-item.muted {
color: var(--text-sub);
opacity: 0.7;
}
.sales-audit-spec {
color: var(--text-sub);
}
.sales-audit-qty {
color: #2563eb;
}
.summary-text {
background: #f7f9fc;
border-radius: 12rpx;
padding: 12rpx;
font-size: 24rpx;
color: var(--text-main);
}
.record-detail {
margin-top: 10rpx;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 6rpx;
font-size: 24rpx;
padding: 8rpx 10rpx;
border-radius: 10rpx;
background: #f7f9fc;
margin-bottom: 10rpx;
}
.detail-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12rpx;
}
.detail-name {
color: var(--text-main);
font-weight: 600;
flex: 1;
word-break: break-all;
}
.detail-qty {
color: var(--text-sub);
white-space: nowrap;
}
.detail-grid {
display: flex;
flex-wrap: wrap;
gap: 8rpx 16rpx;
}
.detail-kv {
display: flex;
gap: 6rpx;
min-width: 45%;
}
.detail-label {
color: var(--text-sub);
white-space: nowrap;
}
.detail-value {
color: var(--text-main);
flex: 1;
word-break: break-all;
}
.detail-empty {
color: var(--text-sub);
font-size: 24rpx;
}
.media-images {
display: flex;
gap: 10rpx;
flex-wrap: wrap;
}
.media-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
}
.media-video video {
width: 100%;
border-radius: 12rpx;
overflow: hidden;
}
.record-actions {
display: flex;
gap: 10rpx;
flex-wrap: wrap;
margin-top: 12rpx;
}
.danger-chip {
color: #f5576c;
border-color: #f6c5d0;
background: #fff2f5;
}
File diff suppressed because it is too large Load Diff