1298 lines
32 KiB
Vue
1298 lines
32 KiB
Vue
<template>
|
||
<view class="page">
|
||
<scroll-view scroll-y class="page-scroll" @scrolltolower="loadMore">
|
||
<common-hero title="审核列表" :use-image-bg="true"></common-hero>
|
||
|
||
<view class="hero-panel">
|
||
<view class="hero-title">审核列表</view>
|
||
<view class="hero-desc">统一处理稿件、评优申报、挑战杯、青马工程、未来学术之星和团员管理审核。</view>
|
||
<view class="hero-meta">
|
||
<view class="metric">
|
||
<view class="metric-value">{{ total }}</view>
|
||
<view class="metric-label">全部记录</view>
|
||
</view>
|
||
<view class="metric">
|
||
<view class="metric-value">{{ pendingCount }}</view>
|
||
<view class="metric-label">待处理</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="filter-card">
|
||
<view class="filter-row">
|
||
<view class="module-filter" @tap="openModulePicker">
|
||
<text class="filter-text">{{ selectedModuleLabel }}</text>
|
||
<text class="filter-arrow">▼</text>
|
||
</view>
|
||
<view class="status-filter" @tap="openStatusPicker">
|
||
<text class="filter-text">{{ selectedStatusLabel }}</text>
|
||
<text class="filter-arrow">▼</text>
|
||
</view>
|
||
<view class="refresh-btn" :class="{ 'refresh-btn--loading': loading }" @tap="reload">
|
||
{{ loading ? '刷新中' : '刷新' }}
|
||
</view>
|
||
</view>
|
||
<view class="search-row">
|
||
<input
|
||
class="search-input"
|
||
v-model="keywords"
|
||
confirm-type="search"
|
||
placeholder="搜索名称/审核意见"
|
||
placeholder-style="color:#a8b2bf;"
|
||
@confirm="reload"
|
||
/>
|
||
<view v-if="keywords" class="clear-btn" @tap="clearKeywords">清除</view>
|
||
<view class="search-btn" @tap="reload">搜索</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="loading && !reviewList.length" class="state-card">
|
||
<view class="state-title">正在加载审核列表...</view>
|
||
<view class="state-desc">请稍候,正在同步后台聚合审核记录。</view>
|
||
</view>
|
||
|
||
<view v-else-if="loadError && !reviewList.length" class="state-card state-card--error">
|
||
<view class="state-title">加载失败</view>
|
||
<view class="state-desc">{{ loadError }}</view>
|
||
<view class="state-action" @tap="reload">重新加载</view>
|
||
</view>
|
||
|
||
<view v-else-if="!reviewList.length" class="state-card">
|
||
<view class="state-title">暂无审核记录</view>
|
||
<view class="state-desc">当前筛选条件下没有待展示的数据。</view>
|
||
</view>
|
||
|
||
<view v-else class="review-list">
|
||
<view v-for="item in reviewList" :key="item.id" class="review-card">
|
||
<view class="card-head">
|
||
<view class="card-main">
|
||
<view class="card-title">{{ item.businessName || '未命名业务' }}</view>
|
||
<view class="card-subtitle">{{ item.moduleName || getModuleName(item.module) }}</view>
|
||
</view>
|
||
<view class="status-tag" :class="`status-tag--${getStatusMeta(item.status).tone}`">
|
||
{{ getStatusMeta(item.status).text }}
|
||
</view>
|
||
</view>
|
||
<view class="info-grid">
|
||
<view class="info-item">
|
||
<view class="info-label">提交时间</view>
|
||
<view class="info-value">{{ formatDateTime(item.createTime) }}</view>
|
||
</view>
|
||
<view class="info-item">
|
||
<view class="info-label">当前审核人</view>
|
||
<view class="info-value">{{ getReviewerName(item) }}</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="item.content" class="content-line">最新意见:{{ item.content }}</view>
|
||
<view class="card-actions">
|
||
<view class="action-btn" @tap="openDataModal(item)">查看数据</view>
|
||
<view class="action-btn action-btn--primary" @tap="openFlowModal(item)">审核流</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="load-more">
|
||
<view v-if="loadingMore" class="load-more-text">正在加载更多...</view>
|
||
<view v-else-if="hasMore" class="load-more-text" @tap="loadMore">点击加载更多</view>
|
||
<view v-else class="load-more-text load-more-text--end">没有更多数据了</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view v-if="showDataModal" class="modal-mask" @tap="closeDataModal">
|
||
<view class="modal-panel" @tap.stop>
|
||
<view class="modal-head">
|
||
<view>
|
||
<view class="modal-title">{{ detailTitle }}</view>
|
||
<view class="modal-subtitle">业务ID:{{ dataCurrent && dataCurrent.pk ? dataCurrent.pk : '-' }}</view>
|
||
</view>
|
||
<view class="modal-close" @tap="closeDataModal">关闭</view>
|
||
</view>
|
||
<scroll-view scroll-y class="modal-body">
|
||
<view v-if="detailLoading" class="state-card state-card--modal">
|
||
<view class="state-title">正在加载详情...</view>
|
||
</view>
|
||
<view v-else-if="detailError" class="state-card state-card--modal state-card--error">
|
||
<view class="state-title">详情加载失败</view>
|
||
<view class="state-desc">{{ detailError }}</view>
|
||
</view>
|
||
<view v-else-if="!detailItems.length" class="state-card state-card--modal">
|
||
<view class="state-title">暂无可展示数据</view>
|
||
</view>
|
||
<view v-else class="detail-list">
|
||
<view v-for="item in detailItems" :key="item.key" class="detail-item">
|
||
<view class="detail-label">{{ item.label }}</view>
|
||
<view v-if="isImageField(item.key) && typeof item.value === 'string'" class="detail-image-wrap">
|
||
<image class="detail-image" :src="item.value" mode="aspectFill" @tap="previewImage(item.value)"/>
|
||
</view>
|
||
<view v-else-if="isFileField(item.key)" class="file-list">
|
||
<view
|
||
v-for="(file, index) in normalizeFileEntries(item.value)"
|
||
:key="`${item.key}-${index}`"
|
||
class="file-item"
|
||
@tap="copyText(file.url, '文件链接已复制')"
|
||
>
|
||
{{ file.name }}
|
||
</view>
|
||
</view>
|
||
<view v-else-if="Array.isArray(item.value)" class="array-list">
|
||
<view v-for="(entry, index) in item.value" :key="`${item.key}-${index}`" class="array-entry">
|
||
<view class="array-index">第{{ index + 1 }}项</view>
|
||
<view v-for="(line, lineIndex) in formatArrayEntry(dataCurrent.module, entry)" :key="lineIndex"
|
||
class="array-line">
|
||
{{ line }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="isPlainObject(item.value)" class="array-list">
|
||
<view v-for="(line, index) in formatObjectLines(dataCurrent.module, item.value)" :key="index"
|
||
class="array-line">
|
||
{{ line }}
|
||
</view>
|
||
</view>
|
||
<view v-else class="detail-value">{{
|
||
formatDisplayValue(dataCurrent.module, item.key, item.value)
|
||
}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showFlowModal" class="modal-mask" @tap="closeFlowModal">
|
||
<view class="modal-panel" @tap.stop>
|
||
<view class="modal-head">
|
||
<view>
|
||
<view class="modal-title">{{ flowTitle }}</view>
|
||
<view class="modal-subtitle">业务ID:{{ flowCurrent && flowCurrent.pk ? flowCurrent.pk : '-' }}</view>
|
||
</view>
|
||
<view class="modal-close" @tap="closeFlowModal">关闭</view>
|
||
</view>
|
||
<scroll-view scroll-y class="modal-body">
|
||
<view v-if="flowLoading" class="state-card state-card--modal">
|
||
<view class="state-title">正在加载审核流...</view>
|
||
</view>
|
||
<view v-else-if="flowError" class="state-card state-card--modal state-card--error">
|
||
<view class="state-title">审核流加载失败</view>
|
||
<view class="state-desc">{{ flowError }}</view>
|
||
</view>
|
||
<view v-else-if="!flowList.length" class="state-card state-card--modal">
|
||
<view class="state-title">暂无审核流数据</view>
|
||
</view>
|
||
<view v-else class="flow-list">
|
||
<view v-for="(item, index) in flowList" :key="item.id" class="flow-item">
|
||
<view class="flow-line">
|
||
<view class="flow-dot" :class="`flow-dot--${getStatusMeta(item.status).tone}`">{{ index + 1 }}</view>
|
||
<view v-if="index < flowList.length - 1" class="flow-rail"></view>
|
||
</view>
|
||
<view class="flow-card">
|
||
<view class="flow-head">
|
||
<view class="flow-user">{{ getReviewerName(item) }}</view>
|
||
<view class="status-tag" :class="`status-tag--${getStatusMeta(item.status).tone}`">
|
||
{{ getStatusMeta(item.status).text }}
|
||
</view>
|
||
</view>
|
||
<view class="flow-time">创建时间:{{ formatDateTime(item.createTime) }}</view>
|
||
<view v-if="item.content" class="flow-content">审核意见:{{ item.content }}</view>
|
||
<view v-if="canReview(item)" class="flow-actions">
|
||
<view class="review-btn review-btn--approve" @tap="openActionModal('approve', item)">通过</view>
|
||
<view class="review-btn review-btn--reject" @tap="openActionModal('reject', item)">拒绝</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="actionVisible" class="action-mask" @tap="closeActionModal">
|
||
<view class="action-panel" @tap.stop>
|
||
<view class="action-title">{{ actionType === 'approve' ? '审核通过' : '审核拒绝' }}</view>
|
||
<textarea
|
||
class="action-textarea"
|
||
:value="actionContent"
|
||
placeholder="请输入审核意见(非必填)"
|
||
placeholder-style="color:#a8b2bf;"
|
||
@input="handleActionInput"
|
||
/>
|
||
<view class="action-footer">
|
||
<view class="ghost-btn" @tap="closeActionModal">取消</view>
|
||
<view class="primary-btn" :class="{ 'primary-btn--loading': submitting }" @tap="submitReview">
|
||
{{ submitting ? '提交中' : '确认' }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<uv-picker
|
||
ref="modulePicker"
|
||
title="选择模块"
|
||
:columns="modulePickerColumns"
|
||
:defaultIndex="[moduleIndex]"
|
||
keyName="text"
|
||
@close="noop"
|
||
@cancel="noop"
|
||
@confirm="confirmModulePicker"
|
||
></uv-picker>
|
||
|
||
<uv-picker
|
||
ref="statusPicker"
|
||
title="选择状态"
|
||
:columns="statusPickerColumns"
|
||
:defaultIndex="[statusIndex]"
|
||
keyName="text"
|
||
@close="noop"
|
||
@cancel="noop"
|
||
@confirm="confirmStatusPicker"
|
||
></uv-picker>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||
import {getCurrentUser} from '../assistant/chat-service'
|
||
import {ensureLoggedIn} from '../../utils/request'
|
||
import {getUserProfile} from '../mine/service'
|
||
import {
|
||
FILE_FIELDS,
|
||
IMAGE_FIELDS,
|
||
MODULE_OPTIONS,
|
||
buildDescriptionItems,
|
||
formatArrayEntry,
|
||
formatDateTime,
|
||
formatDisplayValue,
|
||
formatObjectLines,
|
||
getModuleDetail,
|
||
getModuleName,
|
||
getReviewStatusMeta,
|
||
hasReviewListAuthority,
|
||
isPlainObject,
|
||
listReviewList,
|
||
normalizeFileEntries,
|
||
pageGroupReviewList,
|
||
updateReviewList
|
||
} from '../../utils/gxmu/review-list-service'
|
||
|
||
export default {
|
||
components: {
|
||
CommonHero
|
||
},
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
loadError: '',
|
||
reviewList: [],
|
||
total: 0,
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
loadingMore: false,
|
||
hasMore: true,
|
||
keywords: '',
|
||
selectedModule: '',
|
||
selectedStatus: '',
|
||
dataCurrent: null,
|
||
showDataModal: false,
|
||
detailLoading: false,
|
||
detailError: '',
|
||
detailItems: [],
|
||
flowCurrent: null,
|
||
showFlowModal: false,
|
||
flowLoading: false,
|
||
flowError: '',
|
||
flowList: [],
|
||
actionVisible: false,
|
||
actionType: 'approve',
|
||
actionContent: '',
|
||
actionTarget: null,
|
||
submitting: false,
|
||
loginUserId: 0,
|
||
loginUsername: '',
|
||
loginRealName: ''
|
||
}
|
||
},
|
||
computed: {
|
||
modulePickerColumns() {
|
||
return [[...MODULE_OPTIONS.map((item) => ({text: item.label, value: item.value}))]]
|
||
},
|
||
statusOptions() {
|
||
return [
|
||
{text: '全部状态', value: ''},
|
||
{text: '待审核', value: 0},
|
||
{text: '通过', value: 1},
|
||
{text: '拒绝', value: 2}
|
||
]
|
||
},
|
||
statusPickerColumns() {
|
||
return [[...this.statusOptions]]
|
||
},
|
||
moduleIndex() {
|
||
const index = MODULE_OPTIONS.findIndex((item) => item.value === this.selectedModule)
|
||
return index > -1 ? index : 0
|
||
},
|
||
statusIndex() {
|
||
const index = this.statusOptions.findIndex((item) => item.value === this.selectedStatus)
|
||
return index > -1 ? index : 0
|
||
},
|
||
selectedModuleLabel() {
|
||
const item = MODULE_OPTIONS.find((option) => option.value === this.selectedModule)
|
||
return (item && item.label) || '全部模块'
|
||
},
|
||
selectedStatusLabel() {
|
||
const item = this.statusOptions.find((option) => option.value === this.selectedStatus)
|
||
return (item && item.text) || '全部状态'
|
||
},
|
||
pendingCount() {
|
||
return this.reviewList.filter((item) => Number(item.status) === 0).length
|
||
},
|
||
detailTitle() {
|
||
const current = this.dataCurrent || {}
|
||
return `${current.moduleName || getModuleName(current.module)}详情`
|
||
},
|
||
flowTitle() {
|
||
const current = this.flowCurrent || {}
|
||
return `${current.moduleName || getModuleName(current.module)}审核流`
|
||
}
|
||
},
|
||
onLoad() {
|
||
if (!ensureLoggedIn({redirectUrl: '/pages/gxmu/review-list'})) {
|
||
return
|
||
}
|
||
const user = getCurrentUser()
|
||
this.loginUserId = Number(user.userId || 0)
|
||
this.verifyPermissionAndLoad()
|
||
},
|
||
onPullDownRefresh() {
|
||
this.loadData(true)
|
||
},
|
||
methods: {
|
||
formatDateTime,
|
||
formatDisplayValue,
|
||
formatArrayEntry,
|
||
formatObjectLines,
|
||
isPlainObject,
|
||
normalizeFileEntries,
|
||
getModuleName,
|
||
getStatusMeta(status) {
|
||
return getReviewStatusMeta(status)
|
||
},
|
||
isImageField(key) {
|
||
return IMAGE_FIELDS.has(key)
|
||
},
|
||
isFileField(key) {
|
||
return FILE_FIELDS.has(key)
|
||
},
|
||
showToast(title) {
|
||
uni.showToast({
|
||
title,
|
||
icon: 'none'
|
||
})
|
||
},
|
||
async verifyPermissionAndLoad() {
|
||
try {
|
||
const profile = await getUserProfile()
|
||
if (!hasReviewListAuthority(profile)) {
|
||
this.showToast('暂无审核列表权限')
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 500)
|
||
return
|
||
}
|
||
this.applyLoginUser(profile)
|
||
} catch (error) {
|
||
this.showToast((error && error.message) || '权限校验失败')
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 500)
|
||
return
|
||
}
|
||
this.loadData(true)
|
||
},
|
||
applyLoginUser(profile) {
|
||
const user = profile || {}
|
||
const nestedUser = user.user || {}
|
||
const userId = Number(user.userId || user.id || nestedUser.userId || nestedUser.id || this.loginUserId || 0)
|
||
this.loginUserId = userId || 0
|
||
this.loginUsername = String(user.username || nestedUser.username || '').trim()
|
||
this.loginRealName = String(user.realName || user.name || nestedUser.realName || nestedUser.name || '').trim()
|
||
},
|
||
getReviewerId(item) {
|
||
const user = (item && item.user) || {}
|
||
return Number((item && item.userId) || user.userId || user.id || 0)
|
||
},
|
||
getReviewerIdentityList(item) {
|
||
const user = (item && item.user) || {}
|
||
return [
|
||
user.username,
|
||
user.realName,
|
||
user.name
|
||
].map((value) => String(value || '').trim()).filter(Boolean)
|
||
},
|
||
getReviewerName(item) {
|
||
const user = (item && item.user) || {}
|
||
return user.realName || user.username || '未分配审核人'
|
||
},
|
||
openModulePicker() {
|
||
this.$nextTick(() => {
|
||
this.$refs.modulePicker && this.$refs.modulePicker.open()
|
||
})
|
||
},
|
||
confirmModulePicker(event) {
|
||
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||
this.selectedModule = value
|
||
this.loadData(true)
|
||
},
|
||
openStatusPicker() {
|
||
this.$nextTick(() => {
|
||
this.$refs.statusPicker && this.$refs.statusPicker.open()
|
||
})
|
||
},
|
||
confirmStatusPicker(event) {
|
||
const option = (((event || {}).value || [])[0] || {})
|
||
this.selectedStatus = option.value === undefined ? '' : option.value
|
||
this.loadData(true)
|
||
},
|
||
noop() {
|
||
return
|
||
},
|
||
clearKeywords() {
|
||
this.keywords = ''
|
||
this.reload()
|
||
},
|
||
reload() {
|
||
this.loadData(true)
|
||
},
|
||
async loadData(reset = false) {
|
||
if (this.loading || this.loadingMore) {
|
||
if (reset) {
|
||
uni.stopPullDownRefresh()
|
||
}
|
||
return
|
||
}
|
||
const nextPage = reset ? 1 : this.currentPage
|
||
if (!reset && !this.hasMore) {
|
||
return
|
||
}
|
||
if (reset) {
|
||
this.currentPage = 1
|
||
this.hasMore = true
|
||
this.reviewList = []
|
||
this.total = 0
|
||
this.loading = true
|
||
this.loadError = ''
|
||
} else {
|
||
this.loadingMore = true
|
||
}
|
||
try {
|
||
const params = {
|
||
page: nextPage,
|
||
limit: this.pageSize,
|
||
backendAccess: true
|
||
}
|
||
if (this.selectedModule) params.module = this.selectedModule
|
||
if (this.selectedStatus !== '') params.status = this.selectedStatus
|
||
if (this.keywords) params.keywords = this.keywords
|
||
const result = await pageGroupReviewList(params)
|
||
const list = (result && (result.list || result.rows || result.records)) || []
|
||
const nextList = Array.isArray(list) ? list : []
|
||
const totalValue = result && (result.count !== undefined ? result.count : result.total)
|
||
const hasTotal = totalValue !== undefined && totalValue !== null && totalValue !== ''
|
||
const nextReviewList = reset ? nextList : this.reviewList.concat(nextList)
|
||
this.reviewList = nextReviewList
|
||
this.total = hasTotal ? Number(totalValue || 0) : nextReviewList.length
|
||
this.currentPage = nextPage + 1
|
||
this.hasMore = hasTotal ? nextReviewList.length < this.total : nextList.length >= this.pageSize
|
||
} catch (error) {
|
||
this.loadError = (error && error.message) || '审核列表加载失败'
|
||
this.showToast(this.loadError)
|
||
} finally {
|
||
this.loading = false
|
||
this.loadingMore = false
|
||
uni.stopPullDownRefresh()
|
||
}
|
||
},
|
||
loadMore() {
|
||
if (!this.loading && !this.loadingMore && this.hasMore) {
|
||
this.loadData(false)
|
||
}
|
||
},
|
||
async openDataModal(item) {
|
||
this.dataCurrent = item
|
||
this.showDataModal = true
|
||
this.detailLoading = true
|
||
this.detailError = ''
|
||
this.detailItems = []
|
||
try {
|
||
const detail = await getModuleDetail(item.module, item.pk)
|
||
this.detailItems = buildDescriptionItems(item.module, detail || {})
|
||
} catch (error) {
|
||
this.detailError = (error && error.message) || '数据加载失败'
|
||
} finally {
|
||
this.detailLoading = false
|
||
}
|
||
},
|
||
closeDataModal() {
|
||
this.showDataModal = false
|
||
this.dataCurrent = null
|
||
this.detailItems = []
|
||
this.detailError = ''
|
||
this.detailLoading = false
|
||
},
|
||
async openFlowModal(item) {
|
||
this.flowCurrent = item
|
||
this.showFlowModal = true
|
||
await this.loadFlowData()
|
||
},
|
||
closeFlowModal() {
|
||
this.showFlowModal = false
|
||
this.flowCurrent = null
|
||
this.flowList = []
|
||
this.flowError = ''
|
||
this.flowLoading = false
|
||
this.closeActionModal()
|
||
},
|
||
async loadFlowData() {
|
||
const current = this.flowCurrent || {}
|
||
if (!current.module || !current.pk) {
|
||
this.flowList = []
|
||
return
|
||
}
|
||
this.flowLoading = true
|
||
this.flowError = ''
|
||
try {
|
||
const result = await listReviewList({
|
||
module: current.module,
|
||
pk: current.pk
|
||
})
|
||
const list = Array.isArray(result) ? result : []
|
||
this.flowList = [...list].sort((left, right) => Number(right.id || 0) - Number(left.id || 0))
|
||
} catch (error) {
|
||
this.flowList = []
|
||
this.flowError = (error && error.message) || '审核流加载失败'
|
||
} finally {
|
||
this.flowLoading = false
|
||
}
|
||
},
|
||
canReview(item) {
|
||
if (Number(item && item.status) !== 0) {
|
||
return false
|
||
}
|
||
const reviewerId = this.getReviewerId(item)
|
||
const loginUserId = Number(this.loginUserId || 0)
|
||
if (reviewerId && loginUserId) {
|
||
return reviewerId === loginUserId
|
||
}
|
||
const loginIdentityList = [this.loginUsername, this.loginRealName]
|
||
.map((value) => String(value || '').trim())
|
||
.filter(Boolean)
|
||
if (!loginIdentityList.length) {
|
||
return false
|
||
}
|
||
return this.getReviewerIdentityList(item).some((value) => loginIdentityList.includes(value))
|
||
},
|
||
openActionModal(type, item) {
|
||
if (!this.canReview(item)) {
|
||
this.showToast('当前记录不是由你审核')
|
||
return
|
||
}
|
||
this.actionType = type
|
||
this.actionTarget = item
|
||
this.actionContent = ''
|
||
this.actionVisible = true
|
||
},
|
||
closeActionModal() {
|
||
this.actionVisible = false
|
||
this.actionTarget = null
|
||
this.actionContent = ''
|
||
},
|
||
handleActionInput(event) {
|
||
this.actionContent = (event.detail && event.detail.value) || ''
|
||
},
|
||
async submitReview() {
|
||
if (this.submitting) {
|
||
return
|
||
}
|
||
const target = this.actionTarget
|
||
if (!target || !target.id) {
|
||
this.closeActionModal()
|
||
return
|
||
}
|
||
if (!this.canReview(target)) {
|
||
this.closeActionModal()
|
||
this.showToast('当前记录不是由你审核')
|
||
return
|
||
}
|
||
this.submitting = true
|
||
try {
|
||
await updateReviewList({
|
||
id: target.id,
|
||
status: this.actionType === 'approve' ? 1 : 2,
|
||
module: target.module,
|
||
pk: target.pk,
|
||
sortNumber: target.sortNumber,
|
||
content: String(this.actionContent || '').trim() || undefined
|
||
})
|
||
this.showToast('审核已提交')
|
||
this.closeActionModal()
|
||
await this.loadFlowData()
|
||
await this.loadData(true)
|
||
} catch (error) {
|
||
this.showToast((error && error.message) || '审核提交失败')
|
||
} finally {
|
||
this.submitting = false
|
||
}
|
||
},
|
||
previewImage(url) {
|
||
if (!url) {
|
||
return
|
||
}
|
||
uni.previewImage({
|
||
urls: [url],
|
||
current: url
|
||
})
|
||
},
|
||
copyText(text, title = '已复制') {
|
||
if (!text) {
|
||
this.showToast('暂无可复制内容')
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: String(text),
|
||
success: () => this.showToast(title)
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #f7fbff 0%, #edf4f9 38%, #f6f7f9 100%);
|
||
}
|
||
|
||
.page-scroll {
|
||
height: 100vh;
|
||
padding-bottom: 42rpx;
|
||
}
|
||
|
||
.hero-panel,
|
||
.section {
|
||
margin: 24rpx;
|
||
}
|
||
|
||
.hero-panel {
|
||
padding: 30rpx;
|
||
border-radius: 30rpx;
|
||
background: linear-gradient(135deg, #0d6fba 0%, #1496f2 62%, #5bc2ff 100%);
|
||
box-shadow: 0 18rpx 42rpx rgba(20, 118, 194, 0.16);
|
||
color: #fff;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 38rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.hero-desc {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.65;
|
||
color: rgba(255, 255, 255, 0.86);
|
||
}
|
||
|
||
.hero-meta {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 16rpx;
|
||
margin-top: 28rpx;
|
||
}
|
||
|
||
.metric {
|
||
padding: 20rpx 14rpx;
|
||
border-radius: 22rpx;
|
||
background: rgba(255, 255, 255, 0.13);
|
||
text-align: center;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 34rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.metric-label {
|
||
margin-top: 8rpx;
|
||
font-size: 21rpx;
|
||
color: rgba(255, 255, 255, 0.78);
|
||
}
|
||
|
||
.filter-card,
|
||
.review-card,
|
||
.state-card,
|
||
.flow-card,
|
||
.detail-item {
|
||
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||
background: rgba(255, 255, 255, 0.94);
|
||
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||
}
|
||
|
||
.filter-card {
|
||
padding: 22rpx;
|
||
border-radius: 28rpx;
|
||
}
|
||
|
||
.filter-row,
|
||
.search-row,
|
||
.card-head,
|
||
.card-actions,
|
||
.modal-head,
|
||
.action-footer,
|
||
.flow-head,
|
||
.flow-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-row {
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.module-filter,
|
||
.status-filter,
|
||
.search-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 78rpx;
|
||
border-radius: 22rpx;
|
||
background: #f4f8fb;
|
||
}
|
||
|
||
.module-filter,
|
||
.status-filter {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 22rpx;
|
||
}
|
||
|
||
.status-filter {
|
||
flex: 0 0 190rpx;
|
||
}
|
||
|
||
.filter-text {
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
color: #233142;
|
||
}
|
||
|
||
.filter-arrow {
|
||
font-size: 20rpx;
|
||
color: #7890a8;
|
||
}
|
||
|
||
.refresh-btn,
|
||
.search-btn,
|
||
.clear-btn {
|
||
height: 78rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 22rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
line-height: 78rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.refresh-btn,
|
||
.search-btn {
|
||
background: #1496f2;
|
||
color: #fff;
|
||
}
|
||
|
||
.refresh-btn--loading {
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.search-row {
|
||
gap: 14rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.search-input {
|
||
box-sizing: border-box;
|
||
padding: 0 22rpx;
|
||
font-size: 24rpx;
|
||
color: #263342;
|
||
}
|
||
|
||
.clear-btn {
|
||
background: #eef3f7;
|
||
color: #69798b;
|
||
}
|
||
|
||
.state-card {
|
||
margin-top: 22rpx;
|
||
padding: 34rpx 28rpx;
|
||
border-radius: 28rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.state-card--error {
|
||
border-color: rgba(226, 80, 80, 0.16);
|
||
}
|
||
|
||
.state-card--modal {
|
||
margin-top: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.state-title {
|
||
font-size: 28rpx;
|
||
font-weight: 800;
|
||
color: #263342;
|
||
}
|
||
|
||
.state-desc {
|
||
margin-top: 10rpx;
|
||
font-size: 23rpx;
|
||
line-height: 1.6;
|
||
color: #7a8797;
|
||
}
|
||
|
||
.state-action {
|
||
display: inline-block;
|
||
margin-top: 18rpx;
|
||
padding: 14rpx 24rpx;
|
||
border-radius: 999rpx;
|
||
background: #e8f4ff;
|
||
color: #1496f2;
|
||
font-size: 23rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.review-list {
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.review-card {
|
||
padding: 26rpx;
|
||
border-radius: 28rpx;
|
||
}
|
||
|
||
.review-card + .review-card {
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.card-head {
|
||
justify-content: space-between;
|
||
gap: 18rpx;
|
||
}
|
||
|
||
.card-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 29rpx;
|
||
font-weight: 800;
|
||
color: #222b36;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.card-subtitle {
|
||
margin-top: 8rpx;
|
||
font-size: 23rpx;
|
||
color: #718092;
|
||
}
|
||
|
||
.status-tag {
|
||
padding: 10rpx 18rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.status-tag--success {
|
||
background: #e8f8ef;
|
||
color: #1f9d5b;
|
||
}
|
||
|
||
.status-tag--danger {
|
||
background: #fff0ee;
|
||
color: #d94a3a;
|
||
}
|
||
|
||
.status-tag--warning {
|
||
background: #fff6df;
|
||
color: #b97900;
|
||
}
|
||
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 16rpx;
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.info-item {
|
||
padding: 18rpx;
|
||
border-radius: 20rpx;
|
||
background: #f6f9fc;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 21rpx;
|
||
color: #8a97a6;
|
||
}
|
||
|
||
.info-value {
|
||
margin-top: 8rpx;
|
||
font-size: 23rpx;
|
||
font-weight: 700;
|
||
color: #263342;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.content-line {
|
||
margin-top: 18rpx;
|
||
padding: 18rpx;
|
||
border-radius: 20rpx;
|
||
background: #fffaf0;
|
||
font-size: 23rpx;
|
||
line-height: 1.6;
|
||
color: #7a5b21;
|
||
}
|
||
|
||
.card-actions {
|
||
gap: 16rpx;
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
height: 76rpx;
|
||
border-radius: 22rpx;
|
||
background: #edf4fa;
|
||
color: #2e5c7e;
|
||
font-size: 24rpx;
|
||
font-weight: 800;
|
||
line-height: 76rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.action-btn--primary {
|
||
background: #1496f2;
|
||
color: #fff;
|
||
}
|
||
|
||
.load-more {
|
||
padding: 28rpx 0 8rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.load-more-text {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 220rpx;
|
||
height: 64rpx;
|
||
padding: 0 26rpx;
|
||
border-radius: 999rpx;
|
||
background: #e8f4ff;
|
||
color: #1496f2;
|
||
font-size: 23rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.load-more-text--end {
|
||
background: transparent;
|
||
color: #8a97a6;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-mask,
|
||
.action-mask {
|
||
position: fixed;
|
||
z-index: 20;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
background: rgba(12, 24, 38, 0.46);
|
||
}
|
||
|
||
.modal-panel {
|
||
width: 100%;
|
||
max-height: 86vh;
|
||
border-radius: 34rpx 34rpx 0 0;
|
||
background: #f6f8fa;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.modal-head {
|
||
justify-content: space-between;
|
||
padding: 26rpx 28rpx;
|
||
background: #fff;
|
||
border-bottom: 1rpx solid #edf1f5;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 31rpx;
|
||
font-weight: 800;
|
||
color: #222b36;
|
||
}
|
||
|
||
.modal-subtitle {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #8290a0;
|
||
}
|
||
|
||
.modal-close {
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
background: #eef4f9;
|
||
color: #55697d;
|
||
font-size: 23rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.modal-body {
|
||
box-sizing: border-box;
|
||
max-height: calc(86vh - 112rpx);
|
||
padding: 22rpx;
|
||
}
|
||
|
||
.detail-item {
|
||
padding: 24rpx;
|
||
border-radius: 24rpx;
|
||
}
|
||
|
||
.detail-item + .detail-item {
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.detail-label {
|
||
font-size: 22rpx;
|
||
font-weight: 800;
|
||
color: #7b8998;
|
||
}
|
||
|
||
.detail-value,
|
||
.array-line {
|
||
margin-top: 10rpx;
|
||
font-size: 25rpx;
|
||
line-height: 1.7;
|
||
color: #273443;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.detail-image {
|
||
margin-top: 14rpx;
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
border-radius: 18rpx;
|
||
background: #eef3f7;
|
||
}
|
||
|
||
.file-item {
|
||
margin-top: 12rpx;
|
||
padding: 16rpx 18rpx;
|
||
border-radius: 18rpx;
|
||
background: #edf7ff;
|
||
color: #147cc6;
|
||
font-size: 24rpx;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.array-entry {
|
||
margin-top: 14rpx;
|
||
padding: 18rpx;
|
||
border-radius: 18rpx;
|
||
background: #f5f8fb;
|
||
}
|
||
|
||
.array-entry + .array-entry {
|
||
margin-top: 14rpx;
|
||
}
|
||
|
||
.array-index {
|
||
font-size: 22rpx;
|
||
font-weight: 800;
|
||
color: #41556a;
|
||
}
|
||
|
||
.flow-list {
|
||
padding-bottom: 10rpx;
|
||
}
|
||
|
||
.flow-item {
|
||
display: flex;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.flow-line {
|
||
position: relative;
|
||
width: 66rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.flow-dot {
|
||
position: relative;
|
||
z-index: 2;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 50%;
|
||
font-size: 20rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.flow-dot--success {
|
||
background: #dcf6e8;
|
||
color: #1f9d5b;
|
||
}
|
||
|
||
.flow-dot--danger {
|
||
background: #ffe5e1;
|
||
color: #d94a3a;
|
||
}
|
||
|
||
.flow-dot--warning {
|
||
background: #fff1cc;
|
||
color: #b97900;
|
||
}
|
||
|
||
.flow-rail {
|
||
position: absolute;
|
||
top: 44rpx;
|
||
bottom: 0;
|
||
width: 2rpx;
|
||
background: #d9e4ee;
|
||
}
|
||
|
||
.flow-card {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-bottom: 18rpx;
|
||
padding: 22rpx;
|
||
border-radius: 24rpx;
|
||
}
|
||
|
||
.flow-head {
|
||
justify-content: space-between;
|
||
gap: 14rpx;
|
||
}
|
||
|
||
.flow-user {
|
||
font-size: 27rpx;
|
||
font-weight: 800;
|
||
color: #233142;
|
||
}
|
||
|
||
.flow-time,
|
||
.flow-content {
|
||
margin-top: 12rpx;
|
||
font-size: 23rpx;
|
||
line-height: 1.6;
|
||
color: #748397;
|
||
}
|
||
|
||
.flow-actions {
|
||
gap: 14rpx;
|
||
margin-top: 18rpx;
|
||
}
|
||
|
||
.review-btn {
|
||
flex: 1;
|
||
height: 68rpx;
|
||
border-radius: 20rpx;
|
||
font-size: 23rpx;
|
||
font-weight: 800;
|
||
line-height: 68rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.review-btn--approve {
|
||
background: #1496f2;
|
||
color: #fff;
|
||
}
|
||
|
||
.review-btn--reject {
|
||
background: #fff0ee;
|
||
color: #d94a3a;
|
||
}
|
||
|
||
.action-mask {
|
||
z-index: 30;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 34rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.action-panel {
|
||
width: 100%;
|
||
padding: 28rpx;
|
||
border-radius: 28rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.action-title {
|
||
font-size: 31rpx;
|
||
font-weight: 800;
|
||
color: #233142;
|
||
}
|
||
|
||
.action-textarea {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
min-height: 210rpx;
|
||
margin-top: 22rpx;
|
||
padding: 20rpx;
|
||
border-radius: 22rpx;
|
||
background: #f5f8fb;
|
||
font-size: 24rpx;
|
||
line-height: 1.6;
|
||
color: #263342;
|
||
}
|
||
|
||
.action-footer {
|
||
gap: 16rpx;
|
||
margin-top: 22rpx;
|
||
}
|
||
|
||
.ghost-btn,
|
||
.primary-btn {
|
||
flex: 1;
|
||
height: 76rpx;
|
||
border-radius: 22rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 800;
|
||
line-height: 76rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.ghost-btn {
|
||
background: #eef4f9;
|
||
color: #566b80;
|
||
}
|
||
|
||
.primary-btn {
|
||
background: #1496f2;
|
||
color: #fff;
|
||
}
|
||
|
||
.primary-btn--loading {
|
||
opacity: 0.72;
|
||
}
|
||
</style>
|