feat: 实现协议全文与同意记录功能

新增协议全文后台管理、版本化发布及本人同意记录功能,支持可撤销授权;完善资料与授权相关边界提示,调整评估提交时的资料与授权校验逻辑,同步更新相关页面文案与路由。
This commit is contained in:
hujun
2026-10-14 14:53:13 +08:00
parent b418969e92
commit c95482a0f3
58 changed files with 3269 additions and 146 deletions
+174 -34
View File
@@ -50,9 +50,11 @@
</view>
<view v-else class="upload-area">
<text class="upload-title">资料选择 <text class="optional">(选填)</text></text>
<text class="upload-desc">支持照片,仅在本机预览,尚未上传。</text>
<view class="upload-grid"><view class="upload-card" v-for="(item, index) in uploadNames" :key="item" :class="index == 2 ? 'upload-last' : ''" @click="documentAction(item)"><image v-if="documentPath(item).length > 0" class="document-thumb" :src="documentPath(item)" mode="aspectFill" @error="documentExpired(item)" /><AppIcon v-else name="upload" :size="52" /><text class="upload-name">{{ item }}</text><text :class="documentPath(item).length > 0 ? 'upload-state selected-state' : 'upload-state'">{{ documentPath(item).length > 0 ? '已选择 · 管理' : '点击选择' }}</text></view></view>
<view class="consent"><view class="check-hit" @click="documentConsent = !documentConsent"><view :class="documentConsent ? 'checkbox checked' : 'checkbox'"><text v-if="documentConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('资料使用协议')">《资料使用协议》</text></text></view>
<text class="upload-desc">JPG / PNG,单张最多5MB。仅本人可查看和删除;照片未核验,暂不参与评估。</text>
<view class="upload-grid"><view class="upload-card" v-for="(item, index) in uploadNames" :key="item" :class="index == uploadNames.length - 1 ? 'upload-last' : ''" @click="documentAction(item)"><image v-if="documentPath(item).length > 0" class="document-thumb" :src="documentPath(item)" mode="aspectFill" @error="documentExpired(item, '')" /><AppIcon v-else name="upload" :size="52" /><text class="upload-name">{{ item }}</text><text class="upload-state">{{ documentState(item) }}</text></view></view>
<text v-if="documentMessage.length > 0" class="upload-desc">{{ documentMessage }}</text>
<text v-if="documentRecoveryFailed" class="consent-link" @click="restoreDocuments">照片读取失败 · 点击重试</text>
<view class="consent"><view class="check-hit" @click="toggleDocumentConsent"><view :class="documentConsent ? 'checkbox checked' : 'checkbox'"><text v-if="documentConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('资料使用协议')">《资料使用协议》</text></text></view>
<view class="data-section"><text class="upload-title">数据授权 <text class="optional">(可选)</text></text><text class="upload-desc">自主选择授权项目,可随时取消选择。</text><view class="grant-list"><view v-for="grant in authorizations" :key="grant.key" :class="grant.selected ? 'grant-card grant-selected' : 'grant-card'" @click="grant.selected = !grant.selected"><text :class="grant.selected ? 'grant-name grant-name-active' : 'grant-name'">{{ grant.name }}</text><view :class="grant.selected ? 'checkbox checked' : 'checkbox'"><text v-if="grant.selected" class="check-mark">✓</text></view></view></view><view class="consent"><view class="check-hit" @click="dataConsent = !dataConsent"><view :class="dataConsent ? 'checkbox checked' : 'checkbox'"><text v-if="dataConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('数据授权协议')">《数据授权协议》</text></text></view><text class="local-note">当前仅保存选择意向,不查询外部数据。</text></view>
</view>
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
@@ -74,22 +76,29 @@
import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, getAssessmentInput, formFromInput, documentsFromInput, setAssessmentInput, showAgreementNotice, assetValidationMessage, contributionValidationMessage, clearAssessmentFlow } from '@/services/assessment.uts'
import { contributionOptions as contributionChoices, contributionBaseVisible as baseVisible } from '@/services/contribution.uts'
import { uploadDocument, downloadDocument, deleteDocument, releaseDocumentTemp, documentCategory, fetchOwnedDocuments } from '@/services/documents.uts'
import { Agreement, emptyAgreement, fetchAgreement, grantConsent, consentRequestId } from '@/services/agreements.uts'
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
const contributionOptions = (current: string): string[] => contributionChoices(current)
const contributionBaseVisible = (duration: string): boolean => baseVisible(duration)
const showAgreement = (title: string) => { showAgreementNotice(title) }
const editId = ref(''); const editError = ref(''); const editLoading = ref(false)
let baseRevisionId = ''; let editGeneration = 0
const step = ref(0)
const assessmentType = ref('personal')
const form = ref<AssessmentForm>(emptyAssessmentForm())
let ownerToken = ''; let goingToConfirm = false; let visible = false; let accountTimer = 0
let ownerToken = ''; let goingToConfirm = false; let goingToAgreement = false; let visible = false; let accountTimer = 0
const sameOwner = (): boolean => { const session = getSession(); return session != null && session['token'] == ownerToken }
const customCity = ref('')
const cityChanged = (city: string) => { form.value.city = city; if (city != '其他城市') customCity.value = '' }
const documentConsent = ref(false)
const dataConsent = ref(false)
const documents = ref<SelectedDocument[]>([])
const documentBusy = ref(''); const documentMessage = ref('')
const documentRecoveryFailed = ref(false); let thumbnailIds: string[] = []
let documentGeneration = 0; let documentConsentId = ''; let consentKey = ''; let pendingAgreement: Agreement = emptyAgreement()
let choosingImage = false; let previewingImage = false
const clearDocumentTemps = () => { documents.value.forEach((file) => { if (file.documentId.length > 0) releaseDocumentTemp(file.path); file.path = '' }) }
const invalidateDocuments = () => { documentGeneration++; documentBusy.value = ''; thumbnailIds = []; documentRecoveryFailed.value = false; clearDocumentTemps() }
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '身份证反面', '银行卡', '收入证明'])
@@ -100,7 +109,7 @@
if (sameOwner()) return true
const session = getSession()
ownerToken = session?.getString('token') ?? ''
editGeneration++; initialized = false; stopAccountTimer(); clearAssessmentFlow()
editGeneration++; invalidateDocuments(); documentConsentId = ''; consentKey = ''; pendingAgreement = emptyAgreement(); documentMessage.value = ''; initialized = false; stopAccountTimer(); clearAssessmentFlow()
form.value = emptyAssessmentForm(); documents.value = []; customCity.value = ''; step.value = 0
documentConsent.value = false; dataConsent.value = false; authorizations.value.forEach((item) => { item.selected = false })
baseRevisionId = ''; editLoading.value = false; editError.value = '账户已变化,请返回重新填写。'
@@ -121,6 +130,7 @@
if (currentForm.city == '其他城市' && customCity.value.trim().length > 0) currentForm.city = customCity.value.trim()
setAssessmentInput(assessmentType.value, step.value, currentForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value, editId.value, baseRevisionId)
}
const showAgreement = (title: string) => { if (!ensureOwner()) return; rememberInput(); goingToAgreement = true; showAgreementNotice(title) }
const restore = (type: string) => {
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); customCity.value = ''; documents.value = []; documentConsent.value = false; dataConsent.value = false
authorizations.value.forEach((item) => { item.selected = false })
@@ -136,7 +146,39 @@
initialized = true
rememberInput()
}
const switchType = (type: string) => { if (!ensureOwner() || editId.value.length > 0 || assessmentType.value == type) return; rememberInput(); restore(type); step.value = 0; rememberInput() }
const hydrateDocumentCards = () => {
if (!visible || !sameOwner() || step.value != 3) return
const ticket = documentGeneration
uploadNames.value.forEach((name) => {
const item = documents.value.find((file) => file.name == name)
if (item == null || item.documentId.length == 0 || item.deletePending || item.path.length > 0 || thumbnailIds.includes(item.documentId)) return
const id = item.documentId; thumbnailIds.push(id)
downloadDocument(id).then((path) => {
const current = documents.value.find((file) => file.documentId == id && !file.deletePending)
if (ticket != documentGeneration || !visible || !sameOwner() || current == null) { releaseDocumentTemp(path); return }
if (current.path.length > 0) { releaseDocumentTemp(path); return }
current.path = path
}).catch(() => {
if (ticket == documentGeneration && visible && sameOwner()) documentMessage.value = '缩略图读取失败,点击对应卡片可重试查看'
}).finally(() => { if (ticket == documentGeneration) thumbnailIds = thumbnailIds.filter((value) => value != id) })
})
}
const restoreDocuments = async () => {
if (!ensureOwner() || !initialized || documentBusy.value.length > 0) return
const ticket = documentGeneration; documentBusy.value = 'restore'
try {
const saved = await fetchOwnedDocuments()
if (ticket != documentGeneration || !ensureOwner()) return
const pending = documents.value.filter((file) => file.documentId.length == 0)
documents.value.forEach((file) => { if (file.documentId.length > 0) releaseDocumentTemp(file.path) })
// 卡片先处理待上传项;同类已保存照片通过卡片逐份管理。
documents.value = pending.concat(saved.map((file): SelectedDocument => ({ name: file.name, path: '', documentId: file.documentId, consentId: file.consentId, deletePending: file.deletePending })))
documentRecoveryFailed.value = false; hydrateDocumentCards()
rememberInput()
} catch (error) { if (ticket == documentGeneration && ensureOwner()) { documentRecoveryFailed.value = true; documentMessage.value = error instanceof Error ? error.message : '已保存照片读取失败,请重新读取' } }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const switchType = (type: string) => { if (!ensureOwner() || editId.value.length > 0 || assessmentType.value == type) return; rememberInput(); invalidateDocuments(); restore(type); step.value = 0; rememberInput(); restoreDocuments() }
const loadEdit = async (type: string) => {
const ticket = ++editGeneration; editLoading.value = true
try {
@@ -144,6 +186,7 @@
if (ticket != editGeneration || !ensureOwner()) return
baseRevisionId = saved.getString('baseRevisionId') ?? ''
writeAssessmentFlow(ownerToken, saved); restore(saved.getString('type') ?? type)
restoreDocuments()
} catch (err) { if (ticket == editGeneration && ensureOwner()) editError.value = err instanceof Error ? err.message : '资料读取失败,请返回重试。' }
finally { if (ticket == editGeneration) editLoading.value = false }
}
@@ -161,16 +204,20 @@
})
onShow(() => {
visible = true; stopAccountTimer()
goingToConfirm = false
const returningFromImage = choosingImage || previewingImage
goingToConfirm = false; goingToAgreement = false; previewingImage = false
if (initialized && !sameOwner()) ensureOwner()
const input = getAssessmentInput(assessmentType.value)
if (initialized && input != null && typeof input['step'] == 'number') step.value = Math.max(0, Math.min(3, input['step'] as number))
startAccountTimer()
if (!returningFromImage) restoreDocuments()
})
// 离开填写流程即丢弃未提交内容;进入确认页时只在内存传递。
onHide(() => {
visible = false; stopAccountTimer()
if (!goingToConfirm) {
if (choosingImage || previewingImage) return
invalidateDocuments()
if (!goingToConfirm && !goingToAgreement) {
editGeneration++; if (editId.value.length > 0) editError.value = '已离开修改流程,请返回档案重新进入。'
// 离页清空资料但不切换步骤,避免卸载时又挂载另一组原生输入组件。
clearAssessmentFlow(); form.value = emptyAssessmentForm(); documents.value = []
@@ -178,37 +225,130 @@
authorizations.value.forEach((item) => { item.selected = false })
}
})
onUnload(() => { visible = false; stopAccountTimer(); editGeneration++; initialized = false; clearAssessmentFlow() })
onUnload(() => { visible = false; stopAccountTimer(); editGeneration++; invalidateDocuments(); initialized = false; clearAssessmentFlow() })
// 页内前后步骤只维护当前内存状态,返回入口后不保留填写内容。
onBackPress((): boolean => { rememberInput(); if (step.value > 0) { step.value--; rememberInput(); return true }; return false })
const previous = () => { rememberInput(); if (step.value > 0) step.value--; rememberInput() }
const back = () => { if (step.value > 0) { previous(); return }; rememberInput(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/entry/entry?type=' + assessmentType.value }) }
const documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path }
const documentExpired = (name: string) => {
documents.value = documents.value.filter((file) => file.name != name); rememberInput()
uni.showToast({ title: name + '已失效,请重新选择', icon: 'none' })
const cardDocument = (name: string): SelectedDocument | null => documents.value.find((file) => file.name == name && file.documentId.length == 0) ?? documents.value.find((file) => file.name == name) ?? null
const documentPath = (name: string): string => { const item = cardDocument(name); return item == null ? '' : item.path }
const documentState = (name: string): string => {
if (documentBusy.value == name) return '处理中…'
const item = cardDocument(name)
if (item == null) return '点击上传'
const count = documents.value.filter((file) => file.name == name && file.documentId.length > 0).length
if (item.documentId.length > 0 && count > 1) return count.toString() + ' 张照片 · 管理'
if (item.deletePending) return '删除未完成 · 重试'
return item.documentId.length > 0 ? '已上传 · 管理' : '未上传 · 重试'
}
const documentExpired = (name: string, id: string) => {
const item = id.length == 0 ? cardDocument(name) : documents.value.find((file) => file.name == name && file.documentId == id)
if (item != null) { if (item.documentId.length > 0) releaseDocumentTemp(item.path); item.path = '' }
rememberInput(); documentMessage.value = '预览已失效,已上传资料仍可重新查看或删除'
}
const toggleDocumentConsent = async () => {
if (!ensureOwner() || documentBusy.value.length > 0) return
if (documentConsent.value) { documentConsent.value = false; rememberInput(); return }
const ticket = documentGeneration; documentBusy.value = 'consent'; documentMessage.value = ''
try {
if (pendingAgreement.version.length == 0) {
const agreement = await fetchAgreement('document-use')
if (ticket != documentGeneration || !ensureOwner()) return
pendingAgreement = agreement; consentKey = consentRequestId()
}
const record = await grantConsent(pendingAgreement, consentKey)
if (ticket != documentGeneration || !ensureOwner()) return
if (record.revokedAt.length > 0) { pendingAgreement = emptyAgreement(); consentKey = ''; throw new Error('该同意已撤销,请重新阅读并确认') }
documentConsentId = record.consentId; documentConsent.value = true; pendingAgreement = emptyAgreement(); consentKey = ''; rememberInput()
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '同意记录未确认,请再次点击重试' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const uploadSelectedDocument = async (name: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = documents.value.find((file) => file.name == name && file.documentId.length == 0)
if (item == null || item.path.length == 0 || item.documentId.length > 0) return
if (!documentConsent.value || documentConsentId.length == 0) { documentMessage.value = '请阅读并确认资料使用协议后上传'; return }
const ticket = documentGeneration; documentBusy.value = name; documentMessage.value = ''
try {
const id = await uploadDocument(item.path, documentCategory(name), documentConsentId)
if (ticket != documentGeneration || !ensureOwner()) return
item.documentId = id; item.consentId = documentConsentId; rememberInput()
documentMessage.value = '照片已单独保存,未核验、暂不参与评估'
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '保存结果未确认,请手动重试;可能另存一份照片' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const chooseDocument = (name: string) => {
if (!ensureOwner()) return
const ticket = editGeneration
if (!ensureOwner() || documentBusy.value.length > 0) return
if (!documentConsent.value || documentConsentId.length == 0) { documentMessage.value = '请阅读并确认资料使用协议后上传'; return }
const ticket = documentGeneration; choosingImage = true
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (result) => {
if (ticket != editGeneration || !ensureOwner() || result.tempFilePaths.length == 0) return
documents.value = documents.value.filter((file) => file.name != name)
const document: SelectedDocument = { name, path: result.tempFilePaths[0] }
documents.value.push(document); rememberInput()
}, fail: (error) => { if (ticket == editGeneration && ensureOwner() && !error.errMsg.includes('cancel')) uni.showToast({ title: '选择失败,请重试或检查相册权限', icon: 'none' }) } })
choosingImage = false
if (ticket != documentGeneration || !sameOwner() || result.tempFilePaths.length == 0) return
documents.value = documents.value.filter((file) => file.name != name || file.documentId.length > 0)
const document: SelectedDocument = { name, path: result.tempFilePaths[0], documentId: '', consentId: '', deletePending: false }
documents.value.unshift(document); rememberInput(); uploadSelectedDocument(name)
}, fail: (error) => { choosingImage = false; if (ticket == documentGeneration && sameOwner() && !error.errMsg.includes('cancel')) documentMessage.value = '选择失败,请重试或检查相册权限' } })
}
const previewDocument = async (name: string, id: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = documents.value.find((file) => file.name == name && (id.length == 0 || file.documentId == id))
if (item == null || item.deletePending || item.documentId.length == 0) return
const ticket = documentGeneration; documentBusy.value = name
try {
const path = await downloadDocument(item.documentId)
if (ticket != documentGeneration || !ensureOwner()) { releaseDocumentTemp(path); return }
releaseDocumentTemp(item.path); item.path = path; previewingImage = true
uni.previewImage({ urls: [path], fail: () => { previewingImage = false; documentExpired(name, item.documentId) } })
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '资料读取失败,请重试' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const removeDocument = async (name: string, id: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = documents.value.find((file) => file.name == name && (id.length == 0 || file.documentId == id))
if (item == null) return
if (item.documentId.length == 0) { documents.value = documents.value.filter((file) => file.name != name || file.documentId.length > 0); rememberInput(); return }
const ticket = documentGeneration; documentBusy.value = name; item.deletePending = true; releaseDocumentTemp(item.path); item.path = ''; rememberInput()
try {
await deleteDocument(item.documentId)
if (ticket != documentGeneration || !ensureOwner()) return
documents.value = documents.value.filter((file) => file.documentId != item.documentId); rememberInput(); documentMessage.value = '照片已删除'
hydrateDocumentCards()
} catch (error) { if (ticket == documentGeneration && ensureOwner()) documentMessage.value = error instanceof Error ? error.message : '删除未完成,请对同一资料重试' }
finally { if (ticket == documentGeneration) documentBusy.value = '' }
}
const documentItemAction = (name: string, id: string) => {
if (!ensureOwner() || documentBusy.value.length > 0) return
const ticket = documentGeneration; const item = documents.value.find((file) => file.name == name && file.documentId == id)
if (item == null) { chooseDocument(name); return }
const list = item.deletePending ? ['重试删除'] : (item.documentId.length > 0 ? ['查看照片', '删除照片'] : ['重试上传', '重新选择', '移除未上传照片'])
uni.showActionSheet({ itemList: list, success: (result) => {
if (ticket != documentGeneration || !ensureOwner()) return
if (item.deletePending) removeDocument(name, item.documentId)
else if (item.documentId.length > 0) { if (result.tapIndex == 0) previewDocument(name, item.documentId); else removeDocument(name, item.documentId) }
else { if (result.tapIndex == 0) uploadSelectedDocument(name); else if (result.tapIndex == 1) chooseDocument(name); else removeDocument(name, item.documentId) }
} })
}
function documentPhotoAction(name: string, offset: number): void {
const saved = documents.value.filter((file) => file.name == name && file.documentId.length > 0)
const group = saved.slice(offset, offset + 5); const ticket = documentGeneration
const list = group.map((file, index): string => '照片 ' + (offset + index + 1).toString() + (file.deletePending ? '(删除未完成)' : ''))
const more = offset + group.length < saved.length
// 微信操作菜单最多六项,每次保留五张照片和一个翻页入口。
if (more || offset > 0) list.push(more ? '更多照片' : '返回第一组')
uni.showActionSheet({ itemList: list, success: (result) => {
if (ticket != documentGeneration || !ensureOwner() || result.tapIndex < 0 || result.tapIndex >= list.length) return
if (result.tapIndex == group.length) { documentPhotoAction(name, more ? offset + group.length : 0); return }
documentItemAction(name, group[result.tapIndex].documentId)
} })
}
const documentAction = (name: string) => {
if (!ensureOwner()) return
const ticket = editGeneration
const path = documentPath(name)
if (path.length == 0) { chooseDocument(name); return }
uni.showActionSheet({ itemList: ['预览图片', '重新选择', '移除资料'], success: (result) => {
if (ticket != editGeneration || !ensureOwner()) return
if (result.tapIndex == 0) uni.previewImage({ urls: [path], fail: () => { uni.showToast({ title: '本地图片已失效,请重新选择', icon: 'none' }) } })
else if (result.tapIndex == 1) chooseDocument(name)
else if (result.tapIndex == 2) { documents.value = documents.value.filter((file) => file.name != name); rememberInput() }
} })
if (!ensureOwner() || documentBusy.value.length > 0) return
const item = cardDocument(name)
if (item == null) { chooseDocument(name); return }
if (item.documentId.length == 0) { documentItemAction(name, ''); return }
const saved = documents.value.filter((file) => file.name == name && file.documentId.length > 0)
if (saved.length == 1) { documentItemAction(name, item.documentId); return }
documentPhotoAction(name, 0)
}
const validateStep = (): boolean => {
let message = ''
@@ -226,12 +366,12 @@
}
if (step.value == 1 && message.length == 0 && assessmentType.value == 'personal') message = contributionValidationMessage(form.value)
if (step.value == 2) message = assetValidationMessage(form.value)
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
if (step.value == 3 && (documentBusy.value.length > 0 || documents.value.some((item) => item.documentId.length == 0 || item.deletePending))) message = '请先完成上传、删除重试,或移除未上传照片'
if (step.value == 3 && authorizations.value.some((item) => item.selected) && !dataConsent.value) message = '请阅读并同意数据授权协议,或取消授权选择'
if (message.length == 0) return true
uni.showToast({ title: message, icon: 'none' }); return false
}
const next = () => { if (!ensureOwner() || editLoading.value || editError.value.length > 0) return; if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
const next = () => { if (!ensureOwner() || editLoading.value || editError.value.length > 0) return; if (!validateStep()) return; if (step.value < 3) { step.value++; rememberInput(); hydrateDocumentCards() } else { rememberInput(); goingToConfirm = true; uni.navigateTo({ url: '/packages/finance/confirm/confirm', fail: () => { goingToConfirm = false } }) } }
</script>
<style>
@@ -259,7 +399,7 @@
.upload-grid { flex-direction: row; margin-top: 22rpx; }
.upload-card { flex: 1; min-width: 0; height: 200rpx; margin-right: 14rpx; border-radius: 16rpx; background-color: #f1f6fd; align-items: center; justify-content: center; }
.upload-last { margin-right: 0; }
.document-thumb { width: 74rpx; height: 62rpx; border-radius: 8rpx; }
.document-thumb { width: 100%; height: 100rpx; border-radius: 10rpx; }
.upload-name { font-size: 25rpx; color: #304260; margin-top: 16rpx; }
.upload-state { color: #7a889e; font-size: 22rpx; margin-top: 9rpx; }
.selected-state { color: #0868f7; }