build: 优化docke构建速度
This commit is contained in:
parent
3adf14eac9
commit
8dedce3ca5
51
Dockerfile
51
Dockerfile
|
@ -1,45 +1,34 @@
|
|||
|
||||
# https://stackoverflow.com/questions/53681522/share-variable-in-multi-stage-dockerfile-arg-before-from-not-substituted
|
||||
ARG PROJECT_DIR=/huaxin-base-frontend
|
||||
|
||||
FROM node:20-slim as builder
|
||||
ARG PROJECT_DIR
|
||||
FROM node:20-slim as base
|
||||
ENV PROJECT_DIR=/huaxin-front \
|
||||
PNPM_HOME="/pnpm" \
|
||||
PATH="$PNPM_HOME:$PATH"
|
||||
WORKDIR $PROJECT_DIR
|
||||
COPY ./ $PROJECT_DIR
|
||||
|
||||
# 安装pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
COPY . ./
|
||||
# 安装依赖
|
||||
# 若网络不通,可以使用淘宝源
|
||||
# RUN pnpm config set registry https://registry.npmmirror.com
|
||||
# see https://pnpm.io/docker
|
||||
FROM builder AS prod-deps
|
||||
# 若不存在安装pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# 构建项目
|
||||
# 安装生成依赖
|
||||
FROM base AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM builder AS build
|
||||
# 基于prod-deps生成的依赖执行构建
|
||||
FROM base AS builder
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm run build
|
||||
|
||||
|
||||
# mirror acceleration
|
||||
# RUN npm config set registry https://registry.npmmirror.com
|
||||
# RUN pnpm config set registry https://registry.npmmirror.com
|
||||
# RUN npm config rm proxy && npm config rm https-proxy
|
||||
|
||||
FROM builder
|
||||
# 将prod-deps生成的依赖拷贝到base/$PROJECT_DIR中,开始在builder构建阶段build,最后拷贝build生成的dist到base/$PROJECT_DIR中
|
||||
FROM base AS result
|
||||
COPY --from=prod-deps $PROJECT_DIR/node_modules $PROJECT_DIR/node_modules
|
||||
COPY --from=build $PROJECT_DIR/dist $PROJECT_DIR/dist
|
||||
|
||||
# 构建项目
|
||||
ENV VITE_BASE_URL=/
|
||||
RUN pnpm build
|
||||
|
||||
COPY --from=builder $PROJECT_DIR/dist $PROJECT_DIR/dist
|
||||
|
||||
# 构建nginx,并且在result之后拷贝dist到nginx中,其中包含了自定义nginx.conf
|
||||
FROM nginx:alpine as production
|
||||
ARG PROJECT_DIR
|
||||
|
||||
COPY --from=builder $PROJECT_DIR/dist/ /usr/share/nginx/html
|
||||
COPY --from=builder $PROJECT_DIR/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
ENV PROJECT_DIR=/huaxin-front
|
||||
COPY --from=result $PROJECT_DIR/dist/ /usr/share/nginx/html
|
||||
COPY --from=result $PROJECT_DIR/nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
|
@ -5,7 +5,7 @@
|
|||
<meta name="referrer" content="origin" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="user-scalable=yes" />
|
||||
<title>华信Admin</title>
|
||||
<title>华信办公系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import { request, type RequestOptions } from '@/utils/request';
|
||||
|
||||
/** 获取原材料盘点列表 GET /api/contract */
|
||||
export async function materialsInventoryList(params: API.MaterialsInventoryListParams, options?: RequestOptions) {
|
||||
return request<{
|
||||
items?: API.ContractEntity[];
|
||||
meta?: {
|
||||
itemCount?: number;
|
||||
totalItems?: number;
|
||||
itemsPerPage?: number;
|
||||
totalPages?: number;
|
||||
currentPage?: number;
|
||||
};
|
||||
}>('/api/contract', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
// page has a default value: 1
|
||||
page: '1',
|
||||
// pageSize has a default value: 10
|
||||
pageSize: '10',
|
||||
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增原材料盘点 POST /api/contract */
|
||||
export async function contractCreate(body: API.ContractDto, options?: RequestOptions) {
|
||||
return request<any>('/api/contract', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || { successMsg: '创建成功' }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取原材料盘点信息 GET /api/contract/${param0} */
|
||||
export async function contractInfo(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.ContractInfoParams,
|
||||
options?: RequestOptions,
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<API.ContractEntity>(`/api/contract/${param0}`, {
|
||||
method: 'GET',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 解除原材料盘点和附件关联 PUT /api/contract/unlink-attachments/${param0} */
|
||||
export async function unlinkAttachments(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.ContractUpdateParams,
|
||||
body: API.ContractUpdateDto,
|
||||
options?: RequestOptions,
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/api/contract/unlink-attachments/${param0}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新原材料盘点 PUT /api/contract/${param0} */
|
||||
export async function contractUpdate(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.ContractUpdateParams,
|
||||
body: API.ContractUpdateDto,
|
||||
options?: RequestOptions,
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/api/contract/${param0}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || { successMsg: '更新成功' }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除原材料盘点 DELETE /api/contract/${param0} */
|
||||
export async function contractDelete(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.ContractDeleteParams,
|
||||
options?: RequestOptions,
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/api/contract/${param0}`, {
|
||||
method: 'DELETE',
|
||||
params: { ...queryParams },
|
||||
...(options || { successMsg: '删除成功' }),
|
||||
});
|
||||
}
|
|
@ -1352,4 +1352,56 @@ declare namespace API {
|
|||
type ContractInfoParams = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
type MaterialsInventoryListParams = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
field?: string;
|
||||
order?: 'ASC' | 'DESC';
|
||||
_t?: number;
|
||||
};
|
||||
|
||||
type ContractEntity = {
|
||||
/** 公司名称 */
|
||||
companyName: string;
|
||||
/** 产品名称(字典) */
|
||||
product: number;
|
||||
/** 单位(字典) */
|
||||
unit: number;
|
||||
/** 之前的库存数量 */
|
||||
previousInventoryQuantity: number;
|
||||
/** 之前的单价 */
|
||||
previousUnitPrice: number;
|
||||
/** 之前的金额 */
|
||||
previousAmount: number;
|
||||
/** 入库时间 */
|
||||
inventoryTime: Date;
|
||||
/** 入库数量 */
|
||||
inventoryQuantity: number;
|
||||
/** 入库单价*/
|
||||
inventoryUnitPrice: number;
|
||||
/** 入库金额 */
|
||||
inventoryAmount: number;
|
||||
/** 出库时间 */
|
||||
outime: Date;
|
||||
/** 出库数量 */
|
||||
outQuantity: number;
|
||||
/** 出库单价 */
|
||||
outUnitPrice: number;
|
||||
/** 出库金额 */
|
||||
outAmount: number;
|
||||
/** 现在的结存数量 */
|
||||
currentInventoryQuantity: number;
|
||||
/** 现在的单价 */
|
||||
currentUnitPrice: number;
|
||||
/** 现在的金额 */
|
||||
currentAmount: number;
|
||||
/** 经办人 */
|
||||
agent: string;
|
||||
/** 附件 */
|
||||
files?: any[];
|
||||
id: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
import type { TableColumn } from '@/components/core/dynamic-table';
|
||||
import { ContractStatusEnum } from '@/enums/contractEnum';
|
||||
import { formatToDate } from '@/utils/dateUtil';
|
||||
import { Tag, Button } from 'ant-design-vue';
|
||||
|
||||
export type TableListItem = API.ContractEntity;
|
||||
export type TableColumnItem = TableColumn<TableListItem>;
|
||||
|
||||
export const baseColumns = (ctx: {
|
||||
contractTypes: API.DictItemEntity[];
|
||||
dynamicTableInstance;
|
||||
}): TableColumnItem[] => {
|
||||
const { contractTypes } = ctx;
|
||||
return [
|
||||
{
|
||||
title: '编号',
|
||||
width: 100,
|
||||
maxWidth: 100,
|
||||
fixed: 'left',
|
||||
dataIndex: 'contractNumber',
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
width: 180,
|
||||
dataIndex: 'title',
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
width: 80,
|
||||
formItemProps: {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: contractTypes.map(({ label, id }) => ({ value: id, label })),
|
||||
},
|
||||
},
|
||||
dataIndex: 'type',
|
||||
customRender: ({ record }) => {
|
||||
return contractTypes?.length
|
||||
? contractTypes.find((item) => item.id === record.type)?.label || ''
|
||||
: '';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '甲方',
|
||||
width: 120,
|
||||
dataIndex: 'partyA',
|
||||
},
|
||||
{
|
||||
title: '乙方',
|
||||
width: 120,
|
||||
dataIndex: 'partyB',
|
||||
},
|
||||
{
|
||||
title: '签订时间',
|
||||
width: 60,
|
||||
maxWidth: 60,
|
||||
hideInSearch: true,
|
||||
dataIndex: 'signingDate',
|
||||
customRender: ({ record }) => {
|
||||
return formatToDate(record.signingDate);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '交付期限',
|
||||
width: 60,
|
||||
maxWidth: 60,
|
||||
hideInSearch: true,
|
||||
dataIndex: 'deliveryDeadline',
|
||||
customRender: ({ record }) => {
|
||||
return formatToDate(record.deliveryDeadline);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '审核结果',
|
||||
dataIndex: 'status',
|
||||
maxWidth: 60,
|
||||
width: 60,
|
||||
fixed:'right',
|
||||
formItemProps: {
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: Object.values(ContractStatusEnum)
|
||||
.filter((value) => typeof value === 'number')
|
||||
.map((item) => formatStatus(item as ContractStatusEnum)),
|
||||
},
|
||||
},
|
||||
customRender: ({ record }) => {
|
||||
const { color, label } = formatStatus(record.status);
|
||||
return <Tag color={color}>{label}</Tag>;
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
};
|
||||
|
||||
export function formatStatus(status: ContractStatusEnum): {
|
||||
color: string;
|
||||
label: string;
|
||||
value: number;
|
||||
} {
|
||||
switch (status) {
|
||||
case ContractStatusEnum.Pending:
|
||||
return {
|
||||
color: '#ccc',
|
||||
label: '待审核',
|
||||
value: ContractStatusEnum.Pending,
|
||||
};
|
||||
case ContractStatusEnum.Approved:
|
||||
return {
|
||||
color: 'green',
|
||||
label: '已通过',
|
||||
value: ContractStatusEnum.Approved,
|
||||
};
|
||||
case ContractStatusEnum.Rejected:
|
||||
return {
|
||||
color: 'red',
|
||||
label: '已拒绝',
|
||||
value: ContractStatusEnum.Rejected,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: '#ccc',
|
||||
label: '待审核',
|
||||
value: ContractStatusEnum.Pending,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import type { FormSchema } from '@/components/core/schema-form/';
|
||||
import { ContractStatusEnum } from '@/enums/contractEnum';
|
||||
import { formatStatus } from './columns';
|
||||
|
||||
export const contractSchemas = (
|
||||
contractTypes: API.DictItemEntity[],
|
||||
): FormSchema<API.ContractEntity>[] => [
|
||||
{
|
||||
field: 'contractNumber',
|
||||
component: 'Input',
|
||||
label: '合同编号',
|
||||
rules: [{ required: true, type: 'string' }],
|
||||
colProps: {
|
||||
span: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
component: 'Input',
|
||||
label: '合同标题',
|
||||
rules: [{ required: true, type: 'string' }],
|
||||
colProps: {
|
||||
span: 12,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: 'partyA',
|
||||
component: 'Input',
|
||||
label: '甲方',
|
||||
rules: [{ required: true, type: 'string' }],
|
||||
colProps: {
|
||||
span: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'partyB',
|
||||
component: 'Input',
|
||||
label: '乙方',
|
||||
rules: [{ required: true, type: 'string' }],
|
||||
colProps: {
|
||||
span: 12,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
field: 'signingDate',
|
||||
label: '签订时间',
|
||||
component: 'DatePicker',
|
||||
// defaultValue: new Date(),
|
||||
colProps: { span: 12 },
|
||||
componentProps: {},
|
||||
},
|
||||
{
|
||||
field: 'deliveryDeadline',
|
||||
label: '交付期限',
|
||||
component: 'DatePicker',
|
||||
// defaultValue: new Date(),
|
||||
colProps: { span: 12 },
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '合同类型',
|
||||
component: 'Select',
|
||||
required: true,
|
||||
colProps: {
|
||||
span: 12,
|
||||
},
|
||||
componentProps: {
|
||||
options: contractTypes.map(({ label, id }) => ({ value: id, label })),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '审核结果',
|
||||
component: 'Select',
|
||||
required:true,
|
||||
defaultValue: 0,
|
||||
colProps: {
|
||||
span: 12,
|
||||
},
|
||||
componentProps: {
|
||||
allowClear: false,
|
||||
options: Object.values(ContractStatusEnum)
|
||||
.filter((value) => typeof value === 'number')
|
||||
.map((item) => formatStatus(item as ContractStatusEnum)),
|
||||
},
|
||||
},
|
||||
// {
|
||||
// field: 'remark',
|
||||
// component: 'InputTextArea',
|
||||
// label: '备注',
|
||||
// },
|
||||
// {
|
||||
// field: 'menuIds',
|
||||
// component: 'Tree',
|
||||
// label: '菜单权限',
|
||||
// componentProps: {
|
||||
// checkable: true,
|
||||
// vModelKey: 'checkedKeys',
|
||||
// fieldNames: {
|
||||
// title: 'name',
|
||||
// key: 'id',
|
||||
// },
|
||||
// style: {
|
||||
// height: '350px',
|
||||
// paddingTop: '5px',
|
||||
// overflow: 'auto',
|
||||
// borderRadius: '6px',
|
||||
// border: '1px solid #dcdfe6',
|
||||
// resize: 'vertical',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
];
|
|
@ -1,11 +1,221 @@
|
|||
<template>
|
||||
<div>Meterials Inventory</div>
|
||||
<div v-if="columns?.length">
|
||||
<DynamicTable
|
||||
row-key="id"
|
||||
header-title="合同管理"
|
||||
title-tooltip=""
|
||||
:data-request="Api.contract.contractList"
|
||||
:columns="columns"
|
||||
bordered
|
||||
:scroll="{ x: 1920 }"
|
||||
size="small"
|
||||
>
|
||||
<template #toolbar>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!$auth('system:role:create')"
|
||||
@click="openEditModal({})"
|
||||
>
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
</DynamicTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import { useTable } from '@/components/core/dynamic-table';
|
||||
import { baseColumns, type TableColumnItem, type TableListItem } from './columns';
|
||||
import Api from '@/api/';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { onMounted, ref, type FunctionalComponent } from 'vue';
|
||||
import { DictEnum } from '@/enums/dictEnum';
|
||||
import { useFormModal, useModal } from '@/hooks/useModal';
|
||||
import { contractSchemas } from './formSchemas';
|
||||
import { formatToDate } from '@/utils/dateUtil';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import AttachmentManage from '@/components/business/attachment-manage/index.vue';
|
||||
import AttachmentUpload from '@/components/business/attachment-upload/index.vue';
|
||||
defineOptions({
|
||||
name: 'MeterialsInventory',
|
||||
});
|
||||
const [DynamicTable, dynamicTableInstance] = useTable();
|
||||
const [showModal] = useFormModal();
|
||||
const [fnModal] = useModal();
|
||||
const { getDictItemsByCode } = useDictStore();
|
||||
const contractTypes = ref<API.DictItemEntity[]>([]);
|
||||
|
||||
const getContractTypes = async () => {
|
||||
contractTypes.value = await getDictItemsByCode(DictEnum.ContractType);
|
||||
};
|
||||
const isUploadPopupVisiable = ref(false);
|
||||
|
||||
// contractList;
|
||||
let columns = ref<TableColumnItem[]>();
|
||||
onMounted(() => {
|
||||
getContractTypes().then((res) => {
|
||||
columns.value = [
|
||||
...baseColumns({
|
||||
dynamicTableInstance,
|
||||
contractTypes: contractTypes.value,
|
||||
}),
|
||||
{
|
||||
title: '附件',
|
||||
width: 50,
|
||||
maxWidth: 50,
|
||||
hideInSearch: true,
|
||||
fixed: 'right',
|
||||
dataIndex: 'files',
|
||||
customRender: ({ record }) => <FilesRender {...record} />,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
maxWidth: 80,
|
||||
width: 80,
|
||||
minWidth: 80,
|
||||
dataIndex: 'ACTION',
|
||||
hideInSearch: true,
|
||||
fixed: 'right',
|
||||
actions: ({ record }) => [
|
||||
{
|
||||
icon: 'ant-design:edit-outlined',
|
||||
tooltip: '编辑',
|
||||
auth: {
|
||||
perm: 'app:contract:update',
|
||||
effect: 'disable',
|
||||
},
|
||||
onClick: () => openEditModal(record),
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:delete-outlined',
|
||||
color: 'red',
|
||||
tooltip: '删除此合同',
|
||||
auth: 'app:contract:delete',
|
||||
popConfirm: {
|
||||
title: '你确定要删除吗?',
|
||||
placement: 'left',
|
||||
onConfirm: () => delRowConfirm(record.id),
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'ant-design:cloud-upload-outlined',
|
||||
tooltip: '上传附件',
|
||||
onClick: () => openAttachmentUploadModal(record),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
const openAttachmentUploadModal = async (record: API.ContractEntity) => {
|
||||
isUploadPopupVisiable.value = true;
|
||||
fnModal.show({
|
||||
width: 800,
|
||||
title: `合同编号: ${record.contractNumber}`,
|
||||
content: () => {
|
||||
return (
|
||||
<AttachmentUpload
|
||||
onClose={handleUploadClose}
|
||||
afterUploadCallback={(files) => {
|
||||
afterUploadCallback(files, record.id);
|
||||
}}
|
||||
></AttachmentUpload>
|
||||
);
|
||||
},
|
||||
destroyOnClose: true,
|
||||
open: isUploadPopupVisiable.value,
|
||||
footer: null,
|
||||
});
|
||||
};
|
||||
const handleUploadClose = (hasSuccess: boolean) => {
|
||||
fnModal.hide();
|
||||
isUploadPopupVisiable.value = false;
|
||||
};
|
||||
const afterUploadCallback = async (
|
||||
files: { filename: { path: string; id: number } }[],
|
||||
id: number,
|
||||
) => {
|
||||
await Api.contract.contractUpdate({ id }, { fileIds: files.map((item) => item.filename.id) });
|
||||
dynamicTableInstance?.reload();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 打开新增/编辑弹窗
|
||||
*/
|
||||
const openEditModal = async (record: Partial<TableListItem>) => {
|
||||
const [formRef] = await showModal({
|
||||
modalProps: {
|
||||
title: `${record.id ? '编辑' : '新增'}合同`,
|
||||
width: '50%',
|
||||
onFinish: async (values) => {
|
||||
const params = {
|
||||
...values,
|
||||
signingDate: formatToDate(values.signingDate),
|
||||
deliveryDeadline: formatToDate(values.deliveryDeadline),
|
||||
};
|
||||
if (record.id) {
|
||||
await Api.contract.contractUpdate({ id: record.id }, params);
|
||||
} else {
|
||||
await Api.contract.contractCreate(params);
|
||||
}
|
||||
dynamicTableInstance?.reload();
|
||||
},
|
||||
},
|
||||
formProps: {
|
||||
labelWidth: 100,
|
||||
schemas: contractSchemas(contractTypes.value),
|
||||
},
|
||||
});
|
||||
|
||||
// 如果是编辑的话,需要获取角色详情
|
||||
if (record.id) {
|
||||
const info = await Api.contract.contractInfo({ id: record.id });
|
||||
formRef?.setFieldsValue({
|
||||
...info,
|
||||
});
|
||||
}
|
||||
};
|
||||
const delRowConfirm = async (record) => {
|
||||
await Api.contract.contractDelete({ id: record });
|
||||
dynamicTableInstance?.reload();
|
||||
};
|
||||
|
||||
const FilesRender: FunctionalComponent<TableListItem> = (contract: API.ContractEntity) => {
|
||||
const [fnModal] = useModal();
|
||||
return (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
openFilesManageModal(fnModal, contract);
|
||||
}}
|
||||
>
|
||||
{contract.files?.length || 0}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const openFilesManageModal = (fnModal, contract: API.ContractEntity) => {
|
||||
const fileIds = contract.files?.map((item) => item.id) || [];
|
||||
fnModal.show({
|
||||
width: 1200,
|
||||
title: `附件管理`,
|
||||
content: () => {
|
||||
return (
|
||||
<AttachmentManage
|
||||
fileIds={fileIds}
|
||||
onDelete={(unlinkIds) => unlinkAttachments(contract.id, unlinkIds)}
|
||||
></AttachmentManage>
|
||||
);
|
||||
},
|
||||
destroyOnClose: true,
|
||||
footer: null,
|
||||
});
|
||||
};
|
||||
const unlinkAttachments = async (id: number, unlinkIds: number[]) => {
|
||||
await Api.contract.unlinkAttachments({ id }, { fileIds: unlinkIds });
|
||||
dynamicTableInstance?.reload();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env bash
|
||||
# Use this script to test if a given TCP host/port are available
|
||||
|
||||
WAITFORIT_cmdname=${0##*/}
|
||||
|
||||
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
|
||||
|
||||
usage()
|
||||
{
|
||||
cat << USAGE >&2
|
||||
Usage:
|
||||
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
|
||||
-h HOST | --host=HOST Host or IP under test
|
||||
-p PORT | --port=PORT TCP port under test
|
||||
Alternatively, you specify the host and port as host:port
|
||||
-s | --strict Only execute subcommand if the test succeeds
|
||||
-q | --quiet Don't output any status messages
|
||||
-t TIMEOUT | --timeout=TIMEOUT
|
||||
Timeout in seconds, zero for no timeout
|
||||
-- COMMAND ARGS Execute command with args after the test finishes
|
||||
USAGE
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for()
|
||||
{
|
||||
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
|
||||
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
|
||||
else
|
||||
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
|
||||
fi
|
||||
WAITFORIT_start_ts=$(date +%s)
|
||||
while :
|
||||
do
|
||||
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
|
||||
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
|
||||
WAITFORIT_result=$?
|
||||
else
|
||||
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
|
||||
WAITFORIT_result=$?
|
||||
fi
|
||||
if [[ $WAITFORIT_result -eq 0 ]]; then
|
||||
WAITFORIT_end_ts=$(date +%s)
|
||||
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return $WAITFORIT_result
|
||||
}
|
||||
|
||||
wait_for_wrapper()
|
||||
{
|
||||
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
|
||||
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
|
||||
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
|
||||
else
|
||||
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
|
||||
fi
|
||||
WAITFORIT_PID=$!
|
||||
trap "kill -INT -$WAITFORIT_PID" INT
|
||||
wait $WAITFORIT_PID
|
||||
WAITFORIT_RESULT=$?
|
||||
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
|
||||
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
|
||||
fi
|
||||
return $WAITFORIT_RESULT
|
||||
}
|
||||
|
||||
# process arguments
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case "$1" in
|
||||
*:* )
|
||||
WAITFORIT_hostport=(${1//:/ })
|
||||
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
|
||||
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
|
||||
shift 1
|
||||
;;
|
||||
--child)
|
||||
WAITFORIT_CHILD=1
|
||||
shift 1
|
||||
;;
|
||||
-q | --quiet)
|
||||
WAITFORIT_QUIET=1
|
||||
shift 1
|
||||
;;
|
||||
-s | --strict)
|
||||
WAITFORIT_STRICT=1
|
||||
shift 1
|
||||
;;
|
||||
-h)
|
||||
WAITFORIT_HOST="$2"
|
||||
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--host=*)
|
||||
WAITFORIT_HOST="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
-p)
|
||||
WAITFORIT_PORT="$2"
|
||||
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--port=*)
|
||||
WAITFORIT_PORT="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
-t)
|
||||
WAITFORIT_TIMEOUT="$2"
|
||||
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--timeout=*)
|
||||
WAITFORIT_TIMEOUT="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
WAITFORIT_CLI=("$@")
|
||||
break
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echoerr "Unknown argument: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
|
||||
echoerr "Error: you need to provide a host and port to test."
|
||||
usage
|
||||
fi
|
||||
|
||||
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
|
||||
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
|
||||
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
|
||||
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
|
||||
|
||||
# Check to see if timeout is from busybox?
|
||||
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
|
||||
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
|
||||
|
||||
WAITFORIT_BUSYTIMEFLAG=""
|
||||
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
|
||||
WAITFORIT_ISBUSY=1
|
||||
# Check if busybox timeout uses -t flag
|
||||
# (recent Alpine versions don't support -t anymore)
|
||||
if timeout &>/dev/stdout | grep -q -e '-t '; then
|
||||
WAITFORIT_BUSYTIMEFLAG="-t"
|
||||
fi
|
||||
else
|
||||
WAITFORIT_ISBUSY=0
|
||||
fi
|
||||
|
||||
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
|
||||
wait_for
|
||||
WAITFORIT_RESULT=$?
|
||||
exit $WAITFORIT_RESULT
|
||||
else
|
||||
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
|
||||
wait_for_wrapper
|
||||
WAITFORIT_RESULT=$?
|
||||
else
|
||||
wait_for
|
||||
WAITFORIT_RESULT=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $WAITFORIT_CLI != "" ]]; then
|
||||
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
|
||||
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
|
||||
exit $WAITFORIT_RESULT
|
||||
fi
|
||||
exec "${WAITFORIT_CLI[@]}"
|
||||
else
|
||||
exit $WAITFORIT_RESULT
|
||||
fi
|
Loading…
Reference in New Issue