增加气站合同附件受控上传与预览

This commit is contained in:
czl231
2026-08-18 21:52:00 +08:00
parent 06b6f0ee62
commit 61a61f916b
9 changed files with 262 additions and 9 deletions

View File

@@ -4,9 +4,9 @@
*/
import { getToken } from '@/utils/auth';
const platformApiBaseURL =
const gasApiBaseURL =
import.meta.env.VITE_API_BASE_URL ||
'http://localhost:12426/heqi/platform/v1';
'http://localhost:12426/heqi/gas/v1';
export type ContractAttachmentMetadata = {
has_file: boolean;
@@ -36,7 +36,7 @@ async function upload(file: File): Promise<ContractAttachmentUploadReply> {
const form = new FormData();
form.append('file', file);
const response = await fetch(
`${platformApiBaseURL}/gasorder_contract/attachment/upload`,
`${gasApiBaseURL}/gasorder_contract/attachment/upload`,
{ method: 'POST', headers: authorizationHeaders(), body: form },
);
const payload = (await response.json()) as ApiEnvelope<ContractAttachmentUploadReply>;
@@ -49,7 +49,7 @@ async function upload(file: File): Promise<ContractAttachmentUploadReply> {
/** 使用受签名保护的清理凭证删除尚未绑定的临时文件。 */
async function cleanup(cleanupToken: string): Promise<void> {
const response = await fetch(
`${platformApiBaseURL}/gasorder_contract/attachment/cleanup`,
`${gasApiBaseURL}/gasorder_contract/attachment/cleanup`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authorizationHeaders() },
@@ -64,7 +64,7 @@ async function cleanup(cleanupToken: string): Promise<void> {
/** 获取鉴权 PDF Blob内部存储 URI 始终不会暴露给浏览器。 */
async function load(identity: string): Promise<Blob> {
const response = await fetch(
`${platformApiBaseURL}/gasorder_contract/${encodeURIComponent(identity)}/attachment`,
`${gasApiBaseURL}/gasorder_contract/${encodeURIComponent(identity)}/attachment`,
{ headers: authorizationHeaders() },
);
if (!response.ok) throw new Error('合同附件不可用,请重新上传');

View File

@@ -517,7 +517,7 @@ const gasOverrides: ResourceUiDefinition[] = [
{ name: '重置密码', resource: '/user_account/:identity/password', method: 'PUT', fields: [f('password', { required: true })] },
]),
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true, listCopyable: true }), relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }), relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未指定', placeholder: '请选择默认配送点,可留空', showIdentityCopy: true }), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true, listCopyable: true }), relation('user_account_identity', '/user_account', true, { label: '用户账户', listLabel: '用户账户', listRelationNameOnly: true, showIdentityCopy: true, readonlyRelationText: true }), relation('delivery_basic_identity', '/delivery_basic', false, { label: '所属配送点', listLabel: '所属配送点', listRelationNameOnly: true, emptyText: '暂未指定', placeholder: '请选择默认配送点,可留空', showIdentityCopy: true }), f('title', { required: true }), f('terms'), f('file_uri', { label: '合同附件', type: 'contract-file' }), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [0] } },
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12, 13] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },

View File

@@ -1,6 +1,6 @@
<!--
功能承载气站管理端全部标准资源的新建详情和编辑独立页面
版本v1.4.0
版本v1.5.0
-->
<template>
<div class="record-page">
@@ -52,6 +52,28 @@
:field-options="fieldOptions"
:account-summary="accountSummary"
/>
<a-card
v-if="definition.name === 'gasorder_contract'"
title="合同附件"
:bordered="false"
class="section-card"
>
<a-space>
<span v-if="contractAttachment.available">合同附件.pdf</span>
<a-tag v-else-if="contractAttachment.requiresReupload" color="orange">
旧附件地址不可用请重新上传
</a-tag>
<span v-else>未上传</span>
<a-button
v-if="contractAttachment.available"
type="primary"
:loading="attachmentPreviewing"
@click="previewContractAttachment"
>
预览
</a-button>
</a-space>
</a-card>
<ResourceWalletSummary
v-if="definition.walletOwnerType"
:wallet="wallet"
@@ -127,8 +149,13 @@
:relation-options="relations.options"
:relation-loading="relations.loading"
:role-options="roleOptions"
:contract-attachment="contractAttachmentState"
@change-relation="changeRelation"
@search-relation="relationLinkage.search"
@select-attachment="contractAttachment.select"
@remove-attachment="confirmRemoveContractAttachment"
@clear-attachment-selection="contractAttachment.clearSelection"
@preview-attachment="previewContractAttachment"
/>
</a-form>
<div class="form-actions">
@@ -150,7 +177,7 @@
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue';
import { Message, Modal } from '@arco-design/web-vue';
import { IconEdit, IconLeft } from '@arco-design/web-vue/es/icon';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -199,6 +226,7 @@ import ResourceWalletSummary from './ResourceWalletSummary.vue';
import { createResourceRecordNavigation } from './resource-record-navigation';
import { loadResourceRecordRelations } from './load-resource-record-relations';
import { useResourceAvatar } from './use-resource-avatar';
import { useContractAttachment } from './use-contract-attachment';
import { useResourceRelationLinkage } from './use-resource-relation-linkage';
import { useStaffCredentialOwnerGuard } from './use-staff-credential-owner-guard';
import { useUnsavedRecord } from './use-unsaved-record';
@@ -221,6 +249,7 @@ const form = reactive<Record<string, any>>({});
const loading = ref(true);
const saving = ref(false);
const statusSaving = ref(false);
const attachmentPreviewing = ref(false);
const errorMessage = ref('');
const errorStatus = ref<'403' | '404' | 'error'>('error');
const wallet = ref<ResourceRow>();
@@ -257,10 +286,18 @@ const productOwnershipKeys = [
const actionVisible = ref(false);
const activeAction = ref<DetailAction>();
const avatar = useResourceAvatar();
const contractAttachment = useContractAttachment();
const avatarUrl = avatar.url;
const avatarCanClear = avatar.canClear;
const selectAvatar = avatar.select;
const clearAvatar = avatar.clear;
const contractAttachmentState = computed(() => ({
selectedFile: contractAttachment.selectedFile.value,
hasExisting: contractAttachment.hasExisting.value,
available: contractAttachment.available.value,
requiresReupload: contractAttachment.requiresReupload.value,
removed: contractAttachment.removed.value,
}));
/** 新建智能气阀选择归属时清空其他互斥归属,再执行字段原有联动。 */
async function changeRelation(field: ResourceField, value: unknown) {
@@ -377,7 +414,11 @@ const modeLabel = computed(() =>
const errorTitle = computed(() => recordPageErrorTitle(errorStatus.value));
function snapshot() {
return JSON.stringify({ form, avatar: avatar.marker() });
return JSON.stringify({
form,
avatar: avatar.marker(),
contractAttachment: contractAttachment.marker(),
});
}
const unsaved = useUnsavedRecord(snapshot, () => mode.value !== 'detail');
const { goEdit, viewWallet, goBack, requestBack } =
@@ -414,6 +455,11 @@ async function initialize() {
definition.value.resource,
identity.value,
);
contractAttachment.load(
definition.value.name === 'gasorder_contract'
? (detail.value.attachment as any)
: undefined,
);
if (blockReason.value) {
errorStatus.value = '403';
errorMessage.value = blockReason.value;
@@ -506,6 +552,9 @@ async function save() {
mode.value as 'create' | 'edit',
);
await avatar.applyToPayload(payload);
if (definition.value.name === 'gasorder_contract') {
await contractAttachment.applyToPayload(payload);
}
if (mode.value === 'create') {
const created = await resourceApi.create<ResourceRow>(
definition.value.resource,
@@ -549,6 +598,9 @@ async function save() {
);
}
} catch (error) {
if (definition.value.name === 'gasorder_contract') {
await contractAttachment.rollbackUpload();
}
Message.error((error as Error).message);
} finally {
saving.value = false;
@@ -560,9 +612,36 @@ async function reloadDetail() {
definition.value.resource,
identity.value,
);
if (definition.value.name === 'gasorder_contract') {
contractAttachment.load(detail.value.attachment as any);
}
await Promise.allSettled([loadWallet(), loadAvatar()]);
}
/** 二次确认移除草稿合同附件,真正删除发生在保存成功之后。 */
function confirmRemoveContractAttachment() {
Modal.confirm({
title: '确认移除合同附件?',
content: '移除操作将在保存合同后生效。',
okText: '确认移除',
hideCancel: false,
onOk: () => contractAttachment.remove(),
});
}
/** 获取鉴权 PDF 并在新标签页预览。 */
async function previewContractAttachment() {
if (!recordIdentity.value) return;
attachmentPreviewing.value = true;
try {
await contractAttachment.preview(recordIdentity.value);
} catch (error) {
Message.error((error as Error).message);
} finally {
attachmentPreviewing.value = false;
}
}
async function updateGasStatus(enabled: string | number | boolean) {
statusSaving.value = true;
try {