add product
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
<template>
|
||||
<div v-can="savePermissionCode" class="product-edit">
|
||||
<t-form class="form" label-align="left" label-width="6rem">
|
||||
<t-form-item label="产品编码" required-mark>
|
||||
<t-input v-model.trim="form.code" borderless maxlength="64" placeholder="门店内唯一编码" />
|
||||
</t-form-item>
|
||||
<t-form-item label="产品名称" required-mark>
|
||||
<t-input v-model.trim="form.name" borderless maxlength="128" placeholder="请输入产品名称" />
|
||||
</t-form-item>
|
||||
<t-form-item label="产品类型" arrow content-align="right" @click="openTypePicker">
|
||||
<span class="picker-text" v-text="getProductTypeText(form.productType)" />
|
||||
</t-form-item>
|
||||
<t-form-item label="产品分类" arrow content-align="right" @click="openCategoryPicker">
|
||||
<span class="picker-text" v-text="selectedCategoryName" />
|
||||
</t-form-item>
|
||||
<t-form-item label="基础单位" required-mark>
|
||||
<t-input v-model.trim="form.baseUnit" borderless maxlength="32" placeholder="例如 次、瓶" />
|
||||
</t-form-item>
|
||||
<t-form-item label="参考成本(元)">
|
||||
<t-input v-model.trim="referenceCostText" type="number" align="right" borderless placeholder="非负金额" />
|
||||
</t-form-item>
|
||||
<t-form-item label="库存管理">
|
||||
<t-checkbox v-model="form.stockEnabled" :disabled="form.productType === 'SERVICE'">管理库存</t-checkbox>
|
||||
</t-form-item>
|
||||
<t-form-item label="允许小数">
|
||||
<t-checkbox v-model="form.allowDecimalQuantity">业务数量允许小数</t-checkbox>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="form.stockEnabled" label="最低库存">
|
||||
<t-input v-model.trim="minStockQuantityText" type="number" align="right" borderless placeholder="基础单位数量,最多三位小数" />
|
||||
</t-form-item>
|
||||
<t-form-item label="状态" arrow content-align="right" @click="openStatusPicker">
|
||||
<span class="picker-text" v-text="getProductStatusText(form.status)" />
|
||||
</t-form-item>
|
||||
<t-form-item label="备注">
|
||||
<t-textarea v-model.trim="form.remark" borderless maxlength="255" autosize placeholder="可填写产品说明" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
|
||||
<card-container title="产品附件" description="可上传产品图片或其他产品文件" v-entrance-transition="70">
|
||||
<photo-scroller
|
||||
v-model="attachmentUrlList"
|
||||
v-model:photo-items="attachmentItemList"
|
||||
title="附件列表"
|
||||
:edit-mode="true"
|
||||
:loading="isAttachmentUploading"
|
||||
:show-viewer-actions="false"
|
||||
:add-actions="attachmentAddActions"
|
||||
@add-photo="openAttachmentPicker('PHOTO')"
|
||||
@add-other="openAttachmentPicker('OTHER')"
|
||||
/>
|
||||
<input ref="attachmentInputRef" class="file-input" type="file" accept="image/*" @change="handleAttachmentSelected" />
|
||||
</card-container>
|
||||
|
||||
<card-container title="产品单位" description="必须保留一个基础单位;销售和库存业务只能选择允许的单位" v-entrance-transition="70">
|
||||
<div class="unit-list">
|
||||
<div v-for="(unit, index) in form.unitList" :key="unit.clientKey" class="unit-item">
|
||||
<div class="unit-header">
|
||||
<strong v-text="`单位 ${index + 1}`" />
|
||||
<t-button v-if="1 < form.unitList.length" theme="danger" variant="text" size="small" @click="removeUnit(index)">删除</t-button>
|
||||
</div>
|
||||
<t-form label-width="6rem">
|
||||
<t-form-item label="单位名称" required-mark>
|
||||
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="例如 瓶、盒、次" />
|
||||
</t-form-item>
|
||||
<t-form-item label="基础单位数量" required-mark>
|
||||
<t-input v-model.trim="unit.baseQuantityPerUnit" type="number" align="right" borderless placeholder="每个单位对应的基础单位数量" />
|
||||
</t-form-item>
|
||||
<t-form-item label="建议售价(元)">
|
||||
<t-input v-model.trim="unit.salePriceText" type="number" align="right" borderless placeholder="非负金额" />
|
||||
</t-form-item>
|
||||
<t-form-item label="基础单位">
|
||||
<t-checkbox v-model="unit.isBase" @change="setBaseUnit(index)">基础单位</t-checkbox>
|
||||
</t-form-item>
|
||||
<t-form-item label="允许采购">
|
||||
<t-checkbox v-model="unit.allowPurchase">入库可用</t-checkbox>
|
||||
</t-form-item>
|
||||
<t-form-item label="允许销售">
|
||||
<t-checkbox v-model="unit.allowSale">销售和套餐可用</t-checkbox>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
<t-button class="add-unit" theme="primary" variant="dashed" block @click="addUnit">添加业务单位</t-button>
|
||||
</div>
|
||||
</card-container>
|
||||
|
||||
<t-popup v-model="typePickerVisible" placement="bottom">
|
||||
<t-picker v-model="typeDraft" :columns="productTypeOptions" title="产品类型" @cancel="typePickerVisible = false" @confirm="confirmType" />
|
||||
</t-popup>
|
||||
<t-popup v-model="categoryPickerVisible" placement="bottom">
|
||||
<t-picker v-model="categoryDraft" :columns="categoryOptions" title="产品分类" @cancel="categoryPickerVisible = false" @confirm="confirmCategory" />
|
||||
</t-popup>
|
||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
||||
<t-picker v-model="statusDraft" :columns="statusOptions" title="产品状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
|
||||
</t-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web-api";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { PhotoItem } from "@/types/Photo";
|
||||
import type { Product, ProductAttachmentType, ProductCategory, ProductUnit } from "@/types/Product";
|
||||
import { compressUploadImage } from "@/utils/imageCompression";
|
||||
import { getAttachmentReadURL } from "@/utils/attachment";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { formatProductAmount, getProductStatusText, getProductTypeText, normalizeProductQuantity, parseProductAmount } from "@/utils/product";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
type PickerValue = string | number;
|
||||
type ProductUnitForm = ProductUnit & { clientKey: string; salePriceText: string };
|
||||
type ProductAttachmentItem = PhotoItem & {
|
||||
attachment?: Attachment;
|
||||
attachType: ProductAttachmentType;
|
||||
};
|
||||
|
||||
defineOptions({ name: "ProductEdit" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const navBarStore = useNavBarStore();
|
||||
const userStore = useUserStore();
|
||||
const routeId = computed(() => String(route.params.id || ""));
|
||||
const isCreateMode = computed(() => !routeId.value);
|
||||
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.product.create : permissionCodes.product.update);
|
||||
const isSaving = ref(false);
|
||||
const form = reactive<Product & { unitList: ProductUnitForm[] }>({
|
||||
id: undefined,
|
||||
code: "",
|
||||
name: "",
|
||||
productType: "GOODS",
|
||||
baseUnit: "",
|
||||
stockEnabled: true,
|
||||
allowDecimalQuantity: false,
|
||||
status: "ACTIVE",
|
||||
unitList: []
|
||||
});
|
||||
const referenceCostText = ref("");
|
||||
const minStockQuantityText = ref("");
|
||||
const categoryList = ref<ProductCategory[]>([]);
|
||||
const typePickerVisible = ref(false);
|
||||
const categoryPickerVisible = ref(false);
|
||||
const statusPickerVisible = ref(false);
|
||||
const attachmentInputRef = ref<HTMLInputElement>();
|
||||
const attachmentPickerTarget = ref<ProductAttachmentType>("PHOTO");
|
||||
const isAttachmentUploading = ref(false);
|
||||
const typeDraft = ref<PickerValue[]>([]);
|
||||
const categoryDraft = ref<PickerValue[]>([]);
|
||||
const statusDraft = ref<PickerValue[]>([]);
|
||||
const productTypeOptions = [
|
||||
{ label: "服务", value: "SERVICE" },
|
||||
{ label: "实物", value: "GOODS" },
|
||||
{ label: "消耗品", value: "CONSUMABLE" }
|
||||
];
|
||||
const categoryOptions = computed(() => [{ label: "不设置分类", value: "" }, ...categoryList.value.map(item => ({ label: item.name || "未命名分类", value: item.id || "" }))]);
|
||||
const statusOptions = [
|
||||
{ label: "启用", value: "ACTIVE" },
|
||||
{ label: "停用", value: "INACTIVE" }
|
||||
];
|
||||
const selectedCategoryName = computed(() => categoryList.value.find(item => item.id === form.categoryId)?.name || "不设置分类");
|
||||
const attachmentAddActions = [
|
||||
{
|
||||
icon: "camera",
|
||||
text: "产品图片",
|
||||
emit: "add-photo",
|
||||
theme: "primary"
|
||||
},
|
||||
{
|
||||
icon: "file-attachment",
|
||||
text: "其他文件",
|
||||
emit: "add-other",
|
||||
theme: "default"
|
||||
}
|
||||
] as const;
|
||||
const attachmentItemList = ref<ProductAttachmentItem[]>([]);
|
||||
const attachmentUrlList = computed<string[]>({
|
||||
get() {
|
||||
return attachmentItemList.value.map(item => item.url);
|
||||
},
|
||||
set(value) {
|
||||
const urlSet = new Set(value);
|
||||
attachmentItemList.value = attachmentItemList.value.filter(item => urlSet.has(item.url));
|
||||
}
|
||||
});
|
||||
|
||||
function createUnit(unit?: Partial<ProductUnit>): ProductUnitForm {
|
||||
return {
|
||||
...unit,
|
||||
clientKey: window.crypto.randomUUID(),
|
||||
name: unit?.name || "",
|
||||
baseQuantityPerUnit: unit?.baseQuantityPerUnit ?? "1",
|
||||
salePriceText: unit?.salePrice === null || unit?.salePrice === undefined ? "" : formatProductAmount(unit.salePrice),
|
||||
isBase: unit?.isBase ?? false,
|
||||
allowPurchase: unit?.allowPurchase ?? form.productType !== "SERVICE",
|
||||
allowSale: unit?.allowSale ?? true,
|
||||
sort: unit?.sort ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
function addUnit(): void {
|
||||
form.unitList.push(createUnit({
|
||||
isBase: !form.unitList.length,
|
||||
baseQuantityPerUnit: !form.unitList.length ? "1" : ""
|
||||
}));
|
||||
}
|
||||
|
||||
function removeUnit(index: number): void {
|
||||
if (form.unitList.length < 2) {
|
||||
Message.warning("产品至少需要一个单位");
|
||||
return;
|
||||
}
|
||||
const wasBase = form.unitList[index]?.isBase;
|
||||
form.unitList.splice(index, 1);
|
||||
if (wasBase && form.unitList[0]) {
|
||||
form.unitList[0].isBase = true;
|
||||
form.unitList[0].baseQuantityPerUnit = "1";
|
||||
}
|
||||
}
|
||||
|
||||
function setBaseUnit(index: number): void {
|
||||
form.unitList.forEach((unit, unitIndex) => {
|
||||
unit.isBase = unitIndex === index;
|
||||
});
|
||||
const unit = form.unitList[index];
|
||||
if (unit) {
|
||||
unit.baseQuantityPerUnit = "1";
|
||||
}
|
||||
}
|
||||
|
||||
function openTypePicker(): void {
|
||||
typeDraft.value = [form.productType];
|
||||
typePickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmType(value: Array<string | number>): void {
|
||||
const type = String(value[0] || "");
|
||||
if (type === "SERVICE" || type === "GOODS" || type === "CONSUMABLE") {
|
||||
form.productType = type;
|
||||
if (type === "SERVICE") {
|
||||
form.stockEnabled = false;
|
||||
form.unitList.forEach(unit => {
|
||||
unit.allowPurchase = false;
|
||||
});
|
||||
} else if (form.stockEnabled) {
|
||||
form.unitList.forEach(unit => {
|
||||
unit.allowPurchase = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
typePickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openCategoryPicker(): void {
|
||||
categoryDraft.value = [form.categoryId || ""];
|
||||
categoryPickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmCategory(value: Array<string | number>): void {
|
||||
form.categoryId = String(value[0] || "") || undefined;
|
||||
categoryPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openStatusPicker(): void {
|
||||
statusDraft.value = [form.status || "ACTIVE"];
|
||||
statusPickerVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmStatus(value: Array<string | number>): void {
|
||||
const status = String(value[0] || "");
|
||||
if (status === "ACTIVE" || status === "INACTIVE") {
|
||||
form.status = status;
|
||||
}
|
||||
statusPickerVisible.value = false;
|
||||
}
|
||||
|
||||
function openAttachmentPicker(target: ProductAttachmentType): void {
|
||||
attachmentPickerTarget.value = target;
|
||||
attachmentInputRef.value?.click();
|
||||
}
|
||||
|
||||
async function handleAttachmentSelected(event: Event): Promise<void> {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAttachmentUploading.value = true;
|
||||
try {
|
||||
const tempFile = (await AttachmentAPI.upload(await compressUploadImage(file)))[0];
|
||||
if (tempFile?.id) {
|
||||
attachmentItemList.value.push({
|
||||
attachType: attachmentPickerTarget.value,
|
||||
tempFileId: tempFile.id,
|
||||
url: AttachmentAPI.getTempReadURL(tempFile.id)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "产品附件上传失败"));
|
||||
} finally {
|
||||
isAttachmentUploading.value = false;
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildAttachmentList(): Attachment[] {
|
||||
return attachmentItemList.value.flatMap(item => {
|
||||
if (item.attachment) {
|
||||
return [{
|
||||
...item.attachment,
|
||||
attachType: item.attachType || item.attachment.attachType || "PHOTO"
|
||||
}];
|
||||
}
|
||||
if (item.tempFileId) {
|
||||
return [{
|
||||
attachType: item.attachType,
|
||||
tempFileId: item.tempFileId
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function parseOptionalAmount(value: string): number | null | undefined {
|
||||
if (!value.trim()) {
|
||||
return null;
|
||||
}
|
||||
return parseProductAmount(value);
|
||||
}
|
||||
|
||||
function validate(): boolean {
|
||||
if (!form.code.trim() || !form.name.trim() || !form.baseUnit.trim()) {
|
||||
Message.warning("产品编码、名称和基础单位不能为空");
|
||||
return false;
|
||||
}
|
||||
if (form.productType === "SERVICE" && form.stockEnabled) {
|
||||
Message.warning("服务产品不能管理库存");
|
||||
return false;
|
||||
}
|
||||
const referenceCost = parseOptionalAmount(referenceCostText.value);
|
||||
if (referenceCost === undefined) {
|
||||
Message.warning("价格必须是合法的非负元金额,最多两位小数");
|
||||
return false;
|
||||
}
|
||||
if (form.stockEnabled && !normalizeProductQuantity(minStockQuantityText.value || "0")) {
|
||||
Message.warning("最低库存必须是非负数量,最多三位小数");
|
||||
return false;
|
||||
}
|
||||
if (!form.unitList.length) {
|
||||
Message.warning("至少配置一个产品单位");
|
||||
return false;
|
||||
}
|
||||
const unitNameSet = new Set<string>();
|
||||
let baseCount = 0;
|
||||
for (const unit of form.unitList) {
|
||||
const name = unit.name.trim();
|
||||
const baseQuantityPerUnit = String(unit.baseQuantityPerUnit).trim();
|
||||
const salePrice = parseOptionalAmount(unit.salePriceText);
|
||||
if (!name || unitNameSet.has(name)) {
|
||||
Message.warning("单位名称不能为空且不能重复");
|
||||
return false;
|
||||
}
|
||||
if (!/^\d+(?:\.\d{1,6})?$/.test(baseQuantityPerUnit) || !(0 < Number(baseQuantityPerUnit))) {
|
||||
Message.warning("基础单位数量必须是大于 0 的数字,最多六位小数");
|
||||
return false;
|
||||
}
|
||||
if (salePrice === undefined) {
|
||||
Message.warning("单位售价必须是合法的非负元金额,最多两位小数");
|
||||
return false;
|
||||
}
|
||||
unitNameSet.add(name);
|
||||
if (unit.isBase) {
|
||||
baseCount += 1;
|
||||
if (baseQuantityPerUnit !== "1" && Number(baseQuantityPerUnit) !== 1) {
|
||||
Message.warning("基础单位数量必须为 1");
|
||||
return false;
|
||||
}
|
||||
if (name !== form.baseUnit.trim()) {
|
||||
Message.warning("基础单位名称必须与产品基础单位一致");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (baseCount !== 1) {
|
||||
Message.warning("产品必须且只能有一个基础单位");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
if (isSaving.value || !userStore.hasPermissions(savePermissionCode.value) || !validate()) {
|
||||
return;
|
||||
}
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const minStockQuantity = form.stockEnabled ? normalizeProductQuantity(minStockQuantityText.value || "0") : undefined;
|
||||
const unitList = form.unitList as ProductUnitForm[];
|
||||
const payload: Product = {
|
||||
id: form.id,
|
||||
categoryId: form.categoryId,
|
||||
code: form.code.trim(),
|
||||
name: form.name.trim(),
|
||||
productType: form.productType,
|
||||
baseUnit: form.baseUnit.trim(),
|
||||
stockEnabled: form.stockEnabled,
|
||||
allowDecimalQuantity: form.allowDecimalQuantity,
|
||||
status: form.status,
|
||||
referenceCost: parseOptionalAmount(referenceCostText.value)!,
|
||||
minStockQuantity: form.stockEnabled ? minStockQuantity : null,
|
||||
remark: form.remark?.trim() || null,
|
||||
attachmentList: buildAttachmentList(),
|
||||
unitList: unitList.map((unit, index) => ({
|
||||
id: unit.id,
|
||||
name: unit.name.trim(),
|
||||
baseQuantityPerUnit: String(unit.baseQuantityPerUnit).trim(),
|
||||
salePrice: parseOptionalAmount(unit.salePriceText)!,
|
||||
isBase: !!unit.isBase,
|
||||
allowPurchase: form.productType === "SERVICE" ? false : !!unit.allowPurchase,
|
||||
allowSale: !!unit.allowSale,
|
||||
sort: index
|
||||
}))
|
||||
};
|
||||
if (isCreateMode.value) {
|
||||
const id = await ProductAPI.create(payload);
|
||||
Message.success("产品已创建");
|
||||
await router.replace(`/management/products/${id}`);
|
||||
} else {
|
||||
await ProductAPI.update(payload);
|
||||
Message.success("产品已保存");
|
||||
await router.replace(`/management/products/${routeId.value}`);
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "产品保存失败"));
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const categoryResult = await ProductAPI.categoryList({ index: 0, size: 1000, status: "ACTIVE" });
|
||||
categoryList.value = categoryResult.list || [];
|
||||
if (isCreateMode.value) {
|
||||
addUnit();
|
||||
return;
|
||||
}
|
||||
const result = await ProductAPI.detail(routeId.value);
|
||||
if (result.category && !categoryList.value.some(item => item.id === result.category?.id)) {
|
||||
categoryList.value.push(result.category);
|
||||
}
|
||||
Object.assign(form, {
|
||||
id: result.id,
|
||||
categoryId: result.categoryId,
|
||||
code: result.code || "",
|
||||
name: result.name || "",
|
||||
productType: result.productType || "GOODS",
|
||||
baseUnit: result.baseUnit || "",
|
||||
stockEnabled: !!result.stockEnabled,
|
||||
allowDecimalQuantity: !!result.allowDecimalQuantity,
|
||||
status: result.status || "ACTIVE",
|
||||
remark: result.remark || "",
|
||||
unitList: (result.unitList || []).map(createUnit)
|
||||
});
|
||||
attachmentItemList.value = (result.attachmentList || [])
|
||||
.filter(item => !!item.id)
|
||||
.map(item => ({
|
||||
attachment: item,
|
||||
attachType: item.attachType === "OTHER" ? "OTHER" : "PHOTO",
|
||||
previewUrl: getAttachmentReadURL(item.id || ""),
|
||||
url: getAttachmentReadURL(item.id || "", 90)
|
||||
}));
|
||||
referenceCostText.value = result.referenceCost === null || result.referenceCost === undefined ? "" : `${Math.floor(result.referenceCost / 100)}.${String(result.referenceCost % 100).padStart(2, "0")}`;
|
||||
minStockQuantityText.value = result.minStockQuantity === null || result.minStockQuantity === undefined ? "" : String(result.minStockQuantity);
|
||||
if (!form.unitList.length) {
|
||||
addUnit();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
navBarStore.clearRight();
|
||||
if (userStore.hasPermissions(savePermissionCode.value)) {
|
||||
navBarStore.setRightText("保存", "var(--td-brand-color)", save);
|
||||
}
|
||||
try {
|
||||
await load();
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "产品加载失败"));
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => navBarStore.clearRight());
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.product-edit {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.form {
|
||||
margin: 0 1rem;
|
||||
background: var(--td-bg-color-container);
|
||||
box-shadow: var(--td-shadow-1);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.unit-list {
|
||||
padding: 1rem;
|
||||
|
||||
.unit-item {
|
||||
padding: .75rem 0;
|
||||
border-bottom: 1px dashed var(--td-component-border);
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.t-form__item) {
|
||||
padding: .25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-unit {
|
||||
margin-top: 1rem;
|
||||
border-radius: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user