Files
tuanwei-uniapp/pages/gxmu/review-list.vue
2026-07-20 11:56:02 +08:00

1298 lines
32 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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