Skip to content

Commit f460df7

Browse files
committed
feat: my task fake page
1 parent fb74994 commit f460df7

20 files changed

Lines changed: 997 additions & 113 deletions

File tree

frontend/packages/web/src/assets/icon-font/iconfont.css

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.
0 Bytes
Binary file not shown.
-4 Bytes
Binary file not shown.

frontend/packages/web/src/assets/style/naive-reset.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,10 @@
538538
.n-form-item-feedback__line {
539539
font-size: 12px;
540540
}
541+
.n-input--focus {
542+
--n-box-shadow-focus: none !important;
543+
}
544+
541545
// 滚动条
542546
.n-scrollbar-rail {
543547
z-index: 100;

frontend/packages/web/src/assets/style/var.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@
495495
}
496496
// 模态框title
497497
.crm-modal-title() {
498+
width: calc(100% - 16px);
498499
font-size: 16px;
499500
color: var(--text-n1);
500501

frontend/packages/web/src/components/business/crm-approval-status/index.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
<template>
2-
<div v-if="currentStatus?.label" class="flex items-center gap-[8px]">
2+
<CrmTag
3+
v-if="props.isTag && currentStatus?.label"
4+
:color="{
5+
textColor: currentStatus.tagColor,
6+
color: currentStatus.tagBgColor,
7+
}"
8+
>
9+
{{ currentStatus.label }}
10+
</CrmTag>
11+
<div v-else-if="currentStatus?.label" class="flex items-center gap-[8px]">
312
<CrmIcon :type="currentStatus.icon" :size="16" :class="`text-[${currentStatus.color}]`" />
413
{{ currentStatus.label }}
514
</div>
@@ -9,13 +18,16 @@
918
<script setup lang="ts">
1019
import { ProcessStatusType } from '@lib/shared/models/system/process';
1120
21+
import CrmTag from '@/components/pure/crm-tag/index.vue';
22+
1223
import { processStatusMap } from '@/config/process';
1324
1425
const props = defineProps<{
1526
status: ProcessStatusType;
27+
isTag?: boolean;
1628
}>();
1729
18-
export type StatusInfo = { label: string; icon: string; color: string };
30+
export type StatusInfo = { label: string; icon: string; color: string; tagBgColor: string; tagColor: string };
1931
2032
const currentStatus = computed<StatusInfo | undefined>(() => {
2133
return processStatusMap[props.status as keyof typeof processStatusMap];
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<template>
2+
<div class="crm-file-input-wrapper">
3+
<div
4+
class="z-10 flex h-[24px] w-full items-center rounded-[3px_3px_0_0] border border-b-0 border-[var(--text-n7)] bg-[var(--text-n10)] px-[8px]"
5+
>
6+
<n-tooltip trigger="hover" placement="top-start">
7+
<template #trigger>
8+
<n-upload
9+
v-model:file-list="fileList"
10+
:multiple="props.multiple"
11+
class="crm-file-input-upload"
12+
:show-file-list="false"
13+
:max="10"
14+
@change="({ file, fileList }) => handleFileChange(file as CrmFileItem, fileList as CrmFileItem[])"
15+
@before-upload="({ file, fileList })=>beforeUpload(file as CrmFileItem, fileList as CrmFileItem[])"
16+
>
17+
<CrmIcon type="iconicon_link1" :size="16" class="text-[var(--text-n4)]" />
18+
</n-upload>
19+
</template>
20+
{{ t('crmFormDesign.file') }}
21+
</n-tooltip>
22+
</div>
23+
<n-input
24+
v-model="value"
25+
type="textarea"
26+
:maxlength="300"
27+
:autosize="{
28+
minRows: 3,
29+
}"
30+
class="crm-file-input"
31+
resizable
32+
clearable
33+
show-count
34+
/>
35+
<CrmFileList
36+
v-if="fileList.length > 0"
37+
:files="fileList as unknown as AttachmentInfo[]"
38+
class="mt-[8px]"
39+
@deleteFile="handleDeleteFile"
40+
/>
41+
</div>
42+
</template>
43+
44+
<script setup lang="ts">
45+
import { NInput, NTooltip, NUpload, type UploadFileInfo, useMessage } from 'naive-ui';
46+
47+
import { useI18n } from '@lib/shared/hooks/useI18n';
48+
49+
import CrmIcon from '@/components/pure/crm-icon-font/index.vue';
50+
import { getFileEnum } from '@/components/pure/crm-upload/iconMap';
51+
import type { CrmFileItem } from '@/components/pure/crm-upload/types';
52+
import CrmFileList from '../crm-file-list/index.vue';
53+
54+
import type { AttachmentInfo } from '../crm-form-create/types';
55+
56+
const props = withDefaults(
57+
defineProps<{
58+
multiple?: boolean;
59+
allowRepeat?: boolean;
60+
maxSize?: number;
61+
sizeUnit?: 'MB' | 'KB'; // 文件大小单位
62+
isLimit?: boolean; // 是否限制文件大小
63+
accept?: string; // 接受的文件类型
64+
fileTypeTip?: string; // 文件类型不合法提示
65+
}>(),
66+
{
67+
multiple: true,
68+
allowRepeat: false,
69+
sizeUnit: 'MB',
70+
isLimit: true,
71+
accept: 'none',
72+
}
73+
);
74+
const emit = defineEmits<{
75+
(e: 'change', value: string, fileList: CrmFileItem[]): void;
76+
}>();
77+
78+
const { t } = useI18n();
79+
const Message = useMessage();
80+
81+
const value = defineModel<string>('value', {
82+
required: true,
83+
});
84+
const fileList = defineModel<CrmFileItem[]>('fileList', {
85+
default: () => [],
86+
});
87+
88+
function handleFileChange(file: CrmFileItem, fs: Array<CrmFileItem>) {
89+
const lastFileList = fs.map((e: any) => {
90+
return {
91+
...e,
92+
url: URL.createObjectURL(e.file),
93+
size: e.file.size,
94+
};
95+
});
96+
file.local = true;
97+
file.url = URL.createObjectURL(file.file as Blob);
98+
file.size = file.file?.size;
99+
emit('change', value.value, lastFileList);
100+
}
101+
102+
// 判断文件是否重复
103+
function isFileRepeat(file: CrmFileItem, fs: CrmFileItem[], allowRepeat: boolean): boolean {
104+
if (!allowRepeat) {
105+
const isRepeat = fs.some((item: CrmFileItem) => item.name === file.name && item.local);
106+
return isRepeat;
107+
}
108+
return false;
109+
}
110+
111+
// 判断文件大小
112+
function isFileSizeValid(file: UploadFileInfo, maxSize: number, sizeUnit: string, isLimit: boolean): boolean {
113+
if (isLimit && file.file?.size) {
114+
const maxSizeInBytes = sizeUnit === 'MB' ? maxSize * 1024 * 1024 : maxSize * 1024;
115+
return file.file.size <= maxSizeInBytes;
116+
}
117+
return true;
118+
}
119+
120+
// 判断文件类型
121+
function isFileTypeValid(file: UploadFileInfo, accept?: string): boolean {
122+
const fileFormatMatch = file.name.match(/\.([a-zA-Z0-9]+)$/);
123+
const fileFormatType = fileFormatMatch ? fileFormatMatch[1] : 'none';
124+
return accept === getFileEnum(fileFormatType) || accept === 'none';
125+
}
126+
127+
async function beforeUpload(file: CrmFileItem, _fileList: Array<CrmFileItem>) {
128+
const maxSize = props.maxSize || 50;
129+
130+
// 附件上传校验名称重复
131+
if (fileList.value.length > 0 && isFileRepeat(file, fileList.value, props.allowRepeat)) {
132+
Message.warning(t('crm.upload.repeatFileTip'));
133+
return false; // 文件重复,返回 false 以阻止上传
134+
}
135+
136+
// 校验文件大小
137+
if (!isFileSizeValid(file, maxSize, props.sizeUnit, props.isLimit)) {
138+
Message.warning(t('crm.upload.overSize', { size: maxSize, unit: props.sizeUnit }));
139+
return false; // 文件大小不符合要求,返回 false
140+
}
141+
142+
// 单文件上传时清空之前文件
143+
if (!props.multiple) {
144+
fileList.value = [];
145+
}
146+
147+
// 校验文件类型
148+
if (!isFileTypeValid(file, props.accept)) {
149+
Message.error(props.fileTypeTip || t('crm.upload.fileTypeValidate', { type: props.accept }));
150+
return false;
151+
}
152+
153+
return true;
154+
}
155+
156+
function handleDeleteFile(fileId: string) {
157+
fileList.value = fileList.value.filter((file: CrmFileItem) => file.id !== fileId);
158+
}
159+
</script>
160+
161+
<style lang="less" scoped>
162+
.crm-file-input-wrapper {
163+
@apply flex w-full flex-col;
164+
.crm-file-input-upload {
165+
:deep(.n-upload-trigger) {
166+
@apply flex cursor-pointer items-center;
167+
}
168+
}
169+
.crm-file-input {
170+
border-radius: 0 0 3px 3px;
171+
}
172+
:deep(.n-input--focus) {
173+
box-shadow: none;
174+
}
175+
}
176+
</style>
Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,14 @@
11
<template>
22
<CrmModal v-model:show="show" :title="t('crm.fileListModal.title')" :footer="false">
3-
<div class="flex flex-col gap-[8px]">
4-
<div v-for="file in props.files" :key="file.id" class="crm-file-item">
5-
<div class="flex flex-1 items-center gap-[12px]">
6-
<CrmFileIcon :type="file.type" :size="32" />
7-
<div class="flex flex-1 flex-col gap-[2px]">
8-
<span class="text-[var(--text-n2)]">{{ file.name }}</span>
9-
<div class="flex items-center gap-[8px] text-[12px] text-[var(--text-n4)]">
10-
{{ `${(file.size / 1024).toFixed(2)} KB` }}
11-
{{
12-
t('crm.fileListModal.uploadAt', {
13-
name: file.createUser,
14-
time: dayjs(file.createTime).format('YYYY-MM-DD HH:mm:ss'),
15-
})
16-
}}
17-
</div>
18-
</div>
19-
</div>
20-
<div class="flex items-center gap-[4px]">
21-
<CrmPopConfirm
22-
:title="t('crm.fileListModal.deleteTipTitle')"
23-
icon-type="error"
24-
:content="t('crm.fileListModal.deleteTipContent')"
25-
:positive-text="t('common.confirm')"
26-
trigger="click"
27-
:negative-text="t('common.cancel')"
28-
placement="bottom-end"
29-
@confirm="handleDelete(file, $event)"
30-
>
31-
<n-button :disabled="props.readonly" type="error" text>{{ t('common.delete') }}</n-button>
32-
</CrmPopConfirm>
33-
<template v-if="/(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.type)">
34-
<n-divider vertical />
35-
<n-button type="default" text @click="handlePreview(file)">{{ t('common.preview') }}</n-button>
36-
</template>
37-
<n-divider vertical />
38-
<n-button type="default" text @click="handleDownload(file)">{{ t('common.download') }}</n-button>
39-
</div>
40-
</div>
41-
</div>
42-
<n-image-preview v-model:show="showPreview" :src="previewSrc" />
3+
<CrmFileList :files="props.files" :readonly="props.readonly" @deleteFile="emit('deleteFile', $event)" />
434
</CrmModal>
445
</template>
456

467
<script setup lang="ts">
47-
import { NButton, NDivider, NImagePreview, useMessage } from 'naive-ui';
48-
import dayjs from 'dayjs';
49-
50-
import { PreviewAttachmentUrl } from '@lib/shared/api/requrls/system/module';
518
import { useI18n } from '@lib/shared/hooks/useI18n';
52-
import { isDingTalkBrowser, isLarkBrowser, isWeComBrowser } from '@lib/shared/method';
539
54-
import CrmFileIcon from '@/components/pure/crm-file-icon/index.vue';
5510
import CrmModal from '@/components/pure/crm-modal/index.vue';
56-
import CrmPopConfirm from '@/components/pure/crm-pop-confirm/index.vue';
57-
58-
import { downloadAttachment } from '@/api/modules';
11+
import CrmFileList from '../crm-file-list/index.vue';
5912
6013
import { AttachmentInfo } from '../crm-form-create/types';
6114
@@ -67,54 +20,9 @@
6720
(e: 'deleteFile', id: string): void;
6821
}>();
6922
70-
const Message = useMessage();
7123
const { t } = useI18n();
7224
7325
const show = defineModel<boolean>('show', {
7426
required: true,
7527
});
76-
77-
async function handleDelete(file: AttachmentInfo, close: () => void) {
78-
close();
79-
emit('deleteFile', file.id);
80-
}
81-
82-
const showPreview = ref(false);
83-
const previewSrc = ref('');
84-
function handlePreview(file: AttachmentInfo) {
85-
previewSrc.value = `${PreviewAttachmentUrl}/${file.id}`;
86-
showPreview.value = true;
87-
}
88-
89-
async function handleDownload(file: AttachmentInfo) {
90-
if (isWeComBrowser() || isDingTalkBrowser() || isLarkBrowser()) {
91-
Message.warning(t('crm.fileListModal.wxworkDownloadTip'));
92-
return;
93-
}
94-
try {
95-
const res = await downloadAttachment(file.id);
96-
const url = URL.createObjectURL(new Blob([res], { type: 'application/octet-stream' }));
97-
const a = document.createElement('a');
98-
a.href = url;
99-
a.download = file.name;
100-
document.body.appendChild(a);
101-
a.click();
102-
document.body.removeChild(a);
103-
URL.revokeObjectURL(url);
104-
} catch (error) {
105-
// eslint-disable-next-line no-console
106-
console.log(error);
107-
}
108-
}
10928
</script>
110-
111-
<style lang="less">
112-
.crm-file-item {
113-
@apply flex w-full items-center justify-between;
114-
115-
padding: 8px 12px;
116-
border: 1px solid var(--text-n8);
117-
border-radius: var(--border-radius-small);
118-
background-color: var(--text-n10);
119-
}
120-
</style>

0 commit comments

Comments
 (0)