build: 优化docke构建速度

This commit is contained in:
louis 2024-03-04 09:59:48 +08:00
parent 3adf14eac9
commit 8dedce3ca5
8 changed files with 814 additions and 35 deletions

View File

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

View File

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

View File

@ -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: '删除成功' }),
});
}

View File

@ -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;
};
}

View File

@ -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,
};
}
}

View File

@ -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',
// },
// },
// },
];

View File

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

182
wait-for-it.sh Normal file
View File

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