feat: 优化 banner 配置页面 添加图片裁剪
This commit is contained in:
@@ -58,27 +58,47 @@
|
||||
|
||||
/* ===== 预览 ===== */
|
||||
.previewWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
:global {
|
||||
.ant-spin-nested-loading,
|
||||
.ant-spin-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.previewImg {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: 160px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.previewActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
.uploadAgainBtn {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 提示 ===== */
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
Spin,
|
||||
} from 'ant-design-vue';
|
||||
import { JUMP_TYPE_OPTIONS, EVENT_OPTIONS, REGION_OPTIONS } from '../model/useBannerModel';
|
||||
import { uploadFilesOneByOne, OssUploadType } from '@/utils/oss';
|
||||
import { ImageCropper } from '@/components';
|
||||
import { uploadFile, OssUploadType } from '@/utils/oss';
|
||||
import dayjs from 'dayjs';
|
||||
import styles from './BannerFormModal.module.less';
|
||||
|
||||
@@ -34,6 +35,11 @@ const TITLE_MAX = 50;
|
||||
const PATH_MAX = 200;
|
||||
const URL_MAX = 500;
|
||||
|
||||
/** 图片上传限制:最大 10MB */
|
||||
const IMAGE_MAX_SIZE = 10 * 1024 * 1024;
|
||||
/** 裁剪比例:1053:351 = 3:1 */
|
||||
const CROP_ASPECT_RATIO = 1053 / 351;
|
||||
|
||||
interface BannerFormModalProps {
|
||||
visible: boolean;
|
||||
record: any;
|
||||
@@ -42,6 +48,20 @@ interface BannerFormModalProps {
|
||||
onSave: (formData: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 base64 字符串转换为 File 对象
|
||||
*/
|
||||
function base64ToFile(dataUrl: string, fileName: string): File {
|
||||
const arr = dataUrl.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png';
|
||||
const bstr = atob(arr[1]);
|
||||
const u8arr = new Uint8Array(bstr.length);
|
||||
for (let i = 0; i < bstr.length; i++) {
|
||||
u8arr[i] = bstr.charCodeAt(i);
|
||||
}
|
||||
return new File([u8arr], fileName, { type: mime });
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner 新增/编辑表单弹窗
|
||||
*/
|
||||
@@ -59,6 +79,11 @@ export default defineComponent({
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const formData = reactive(getDefaultForm());
|
||||
|
||||
/** 裁剪弹窗状态 */
|
||||
const cropperVisible = ref(false);
|
||||
/** 待裁剪图片的本地预览 URL */
|
||||
const uploadImageUrl = ref('');
|
||||
|
||||
/** 根据 record 初始化表单 */
|
||||
const initFormFromRecord = (record: any) => {
|
||||
const fresh = getDefaultForm();
|
||||
@@ -99,6 +124,8 @@ export default defineComponent({
|
||||
(val) => {
|
||||
if (val) {
|
||||
initFormFromRecord(props.record);
|
||||
cropperVisible.value = false;
|
||||
uploadImageUrl.value = '';
|
||||
setTimeout(() => formRef.value?.clearValidate(), 0);
|
||||
}
|
||||
},
|
||||
@@ -126,8 +153,8 @@ export default defineComponent({
|
||||
fileInputRef.value?.click();
|
||||
};
|
||||
|
||||
/** 处理文件选择 */
|
||||
const handleFileChange = async (e: Event) => {
|
||||
/** 处理文件选择 → 打开裁剪弹窗 */
|
||||
const handleFileChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
// 重置 input 以便同一文件可再次选择
|
||||
@@ -138,40 +165,61 @@ export default defineComponent({
|
||||
message.error('请选择图片文件');
|
||||
return;
|
||||
}
|
||||
const MAX_SIZE = 5 * 1024 * 1024;
|
||||
if (file.size > MAX_SIZE) {
|
||||
message.error('图片大小不能超过 5MB');
|
||||
if (file.size > IMAGE_MAX_SIZE) {
|
||||
message.error(`图片大小不能超过 ${IMAGE_MAX_SIZE / 1024 / 1024}MB`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地预览
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
// 生成本地预览 URL,打开裁剪弹窗
|
||||
uploadImageUrl.value = URL.createObjectURL(file);
|
||||
cropperVisible.value = true;
|
||||
};
|
||||
|
||||
/** 裁剪确认回调:将裁剪后的图片上传到 OSS */
|
||||
const handleCropConfirm = async (dataUrl: string) => {
|
||||
cropperVisible.value = false;
|
||||
|
||||
// 释放旧的本地预览
|
||||
if (formData.imageUrl && formData.imageUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(formData.imageUrl);
|
||||
}
|
||||
|
||||
// 先用 base64 做本地预览
|
||||
const localUrl = dataUrl;
|
||||
formData.imageUrl = localUrl;
|
||||
formData.imageStatus = 'uploading';
|
||||
|
||||
try {
|
||||
await uploadFilesOneByOne([file], OssUploadType.Banner, {
|
||||
onSuccess: (url) => {
|
||||
URL.revokeObjectURL(localUrl);
|
||||
formData.imageUrl = url;
|
||||
formData.imageStatus = 'success';
|
||||
message.success('图片上传成功');
|
||||
},
|
||||
onError: (err, idx) => {
|
||||
console.error(`[Banner] 上传失败 (${idx}):`, err);
|
||||
URL.revokeObjectURL(localUrl);
|
||||
formData.imageUrl = '';
|
||||
formData.imageStatus = 'error';
|
||||
message.error('图片上传失败');
|
||||
},
|
||||
});
|
||||
const file = base64ToFile(dataUrl, `banner_${Date.now()}.jpg`);
|
||||
const result = await uploadFile(file, OssUploadType.Banner as any);
|
||||
|
||||
// 释放本地预览
|
||||
URL.revokeObjectURL(localUrl);
|
||||
formData.imageUrl = result.url;
|
||||
formData.imageStatus = 'success';
|
||||
message.success('图片上传成功');
|
||||
} catch (err: any) {
|
||||
console.error('[Banner] 获取上传凭证失败:', err);
|
||||
console.error('[Banner] 上传失败:', err);
|
||||
URL.revokeObjectURL(localUrl);
|
||||
formData.imageUrl = '';
|
||||
formData.imageStatus = 'error';
|
||||
message.error(err?.message || '图片上传失败');
|
||||
}
|
||||
|
||||
// 清理裁剪资源
|
||||
if (uploadImageUrl.value) {
|
||||
URL.revokeObjectURL(uploadImageUrl.value);
|
||||
}
|
||||
uploadImageUrl.value = '';
|
||||
};
|
||||
|
||||
/** 裁剪弹窗取消 */
|
||||
const onCropperCancel = () => {
|
||||
cropperVisible.value = false;
|
||||
if (uploadImageUrl.value) {
|
||||
URL.revokeObjectURL(uploadImageUrl.value);
|
||||
}
|
||||
uploadImageUrl.value = '';
|
||||
};
|
||||
|
||||
/** 删除已上传图片 */
|
||||
@@ -214,266 +262,292 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
return () => (
|
||||
<Modal
|
||||
title={props.record ? '编辑 Banner' : '新增 Banner'}
|
||||
visible={props.visible}
|
||||
onCancel={props.onClose}
|
||||
width={640}
|
||||
destroyOnClose
|
||||
footer={null}
|
||||
centered
|
||||
>
|
||||
<Form ref={formRef} layout="vertical" model={formData} class={styles.form} requiredMark>
|
||||
{/* ===== Banner 标题 ===== */}
|
||||
<Form.Item
|
||||
label="Banner标题"
|
||||
name="title"
|
||||
rules={[
|
||||
{ required: true, message: '请输入 Banner 标题' },
|
||||
{ max: TITLE_MAX, message: `标题最多 ${TITLE_MAX} 个字符` },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入标题"
|
||||
maxlength={TITLE_MAX}
|
||||
allowClear
|
||||
v-model:value={formData.title}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 上传图片 ===== */}
|
||||
<Form.Item
|
||||
label="上传图片"
|
||||
name="imageUrl"
|
||||
rules={[
|
||||
{
|
||||
validator: async () => {
|
||||
if (!formData.imageUrl || formData.imageUrl.startsWith('blob:')) {
|
||||
return Promise.reject(new Error('请上传 Banner 图片'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div class={styles.uploader}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{formData.imageStatus === 'empty' || formData.imageStatus === 'error' ? (
|
||||
<div
|
||||
class={`${styles.uploadCard} ${
|
||||
formData.imageStatus === 'error' ? styles.uploadCardError : ''
|
||||
}`}
|
||||
onClick={triggerFilePicker}
|
||||
>
|
||||
<span class={styles.uploadIcon}>+</span>
|
||||
<span class={styles.uploadHint}>点击上传</span>
|
||||
<span class={styles.uploadTip}>
|
||||
建议尺寸 1053*351 像素
|
||||
<br />
|
||||
最多上传 1 张
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div class={styles.previewWrapper}>
|
||||
<Spin spinning={formData.imageStatus === 'uploading'} tip="上传中...">
|
||||
<img class={styles.previewImg} src={formData.imageUrl} alt="banner preview" />
|
||||
</Spin>
|
||||
<div class={styles.previewActions}>
|
||||
<Button size="small" onClick={triggerFilePicker}>
|
||||
替换
|
||||
</Button>
|
||||
<Button size="small" danger onClick={handleRemoveImage}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 跳转类型 ===== */}
|
||||
<Form.Item
|
||||
label="跳转类型"
|
||||
name="jumpType"
|
||||
rules={[{ required: true, message: '请选择跳转类型' }]}
|
||||
>
|
||||
<Select
|
||||
options={JUMP_TYPE_OPTIONS as any}
|
||||
placeholder="请选择"
|
||||
v-model:value={formData.jumpType}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 联动字段:根据跳转类型动态展示 ===== */}
|
||||
{formData.jumpType === 'event_detail' && (
|
||||
<>
|
||||
<Modal
|
||||
title={props.record ? '编辑 Banner' : '新增 Banner'}
|
||||
visible={props.visible}
|
||||
onCancel={props.onClose}
|
||||
width={640}
|
||||
destroyOnClose
|
||||
footer={null}
|
||||
centered
|
||||
>
|
||||
<Form ref={formRef} layout="vertical" model={formData} class={styles.form} requiredMark>
|
||||
{/* ===== Banner 标题 ===== */}
|
||||
<Form.Item
|
||||
label="关联赛事"
|
||||
name="relatedEventId"
|
||||
rules={[{ required: true, message: '请选择关联赛事' }]}
|
||||
>
|
||||
<Select
|
||||
options={EVENT_OPTIONS as any}
|
||||
placeholder="不关联"
|
||||
allowClear
|
||||
v-model:value={formData.relatedEventId}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{formData.jumpType === 'miniprogram_page' && (
|
||||
<Form.Item
|
||||
label="页面路径"
|
||||
name="pagePath"
|
||||
label="Banner标题"
|
||||
name="title"
|
||||
rules={[
|
||||
{ required: true, message: '请输入页面路径' },
|
||||
{
|
||||
pattern: /^\//,
|
||||
message: '页面路径需以 / 开头',
|
||||
},
|
||||
{ max: PATH_MAX, message: `路径最多 ${PATH_MAX} 个字符` },
|
||||
{ required: true, message: '请输入' },
|
||||
{ max: TITLE_MAX, message: `标题最多 ${TITLE_MAX} 个字符` },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="/pages/index/index"
|
||||
maxlength={PATH_MAX}
|
||||
placeholder="请输入标题"
|
||||
maxlength={TITLE_MAX}
|
||||
allowClear
|
||||
v-model:value={formData.pagePath}
|
||||
v-model:value={formData.title}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{formData.jumpType === 'custom_link' && (
|
||||
{/* ===== 上传图片(必填,带裁剪) ===== */}
|
||||
<Form.Item
|
||||
label="自定义链接"
|
||||
name="customUrl"
|
||||
label="上传图片"
|
||||
name="imageUrl"
|
||||
rules={[
|
||||
{ required: true, message: '请输入自定义链接' },
|
||||
{
|
||||
pattern: /^https?:\/\//i,
|
||||
message: '请输入以 http/https 开头的链接',
|
||||
},
|
||||
{ max: URL_MAX, message: `链接最多 ${URL_MAX} 个字符` },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="https://xxx"
|
||||
maxlength={URL_MAX}
|
||||
allowClear
|
||||
v-model:value={formData.customUrl}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{formData.jumpType === 'none' && (
|
||||
<div class={styles.tip}>选择"无链接"时,点击 Banner 不会跳转。</div>
|
||||
)}
|
||||
|
||||
{/* ===== 所属区域 ===== */}
|
||||
<Form.Item
|
||||
label="所属区域"
|
||||
name="region"
|
||||
rules={[{ required: true, message: '请选择所属区域' }]}
|
||||
>
|
||||
<Select
|
||||
options={REGION_OPTIONS as any}
|
||||
placeholder="首页"
|
||||
v-model:value={formData.region}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 展示时间(开始/结束) ===== */}
|
||||
<div class={styles.timeRow}>
|
||||
<Form.Item
|
||||
label="开始时间"
|
||||
name="displayStartTime"
|
||||
class={styles.timeItem}
|
||||
rules={[{ required: true, message: '请选择开始时间' }]}
|
||||
>
|
||||
<DatePicker
|
||||
placeholder="年-月-日"
|
||||
format="YYYY-MM-DD"
|
||||
value={
|
||||
formData.displayTimeRange?.[0] ? dayjs(formData.displayTimeRange[0]) : undefined
|
||||
}
|
||||
onUpdate:value={(v: any) => {
|
||||
formData.displayTimeRange = [
|
||||
v ? v.format('YYYY-MM-DD') : '',
|
||||
formData.displayTimeRange?.[1] || '',
|
||||
];
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="结束时间"
|
||||
name="displayEndTime"
|
||||
class={styles.timeItem}
|
||||
rules={[
|
||||
{ required: true, message: '请选择结束时间' },
|
||||
{ required: true, message: '请上传 Banner 图片' },
|
||||
{
|
||||
validator: async () => {
|
||||
const [s, e] = formData.displayTimeRange || [];
|
||||
if (s && e && new Date(e) < new Date(s)) {
|
||||
return Promise.reject(new Error('结束时间不能早于开始时间'));
|
||||
if (!formData.imageUrl || formData.imageUrl.startsWith('blob:')) {
|
||||
return Promise.reject(new Error('请上传 Banner 图片'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
placeholder="年-月-日"
|
||||
format="YYYY-MM-DD"
|
||||
value={
|
||||
formData.displayTimeRange?.[1] ? dayjs(formData.displayTimeRange[1]) : undefined
|
||||
}
|
||||
onUpdate:value={(v: any) => {
|
||||
formData.displayTimeRange = [
|
||||
formData.displayTimeRange?.[0] || '',
|
||||
v ? v.format('YYYY-MM-DD') : '',
|
||||
];
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
<div class={styles.uploader}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
{formData.imageStatus === 'empty' || formData.imageStatus === 'error' ? (
|
||||
<div
|
||||
class={`${styles.uploadCard} ${
|
||||
formData.imageStatus === 'error' ? styles.uploadCardError : ''
|
||||
}`}
|
||||
onClick={triggerFilePicker}
|
||||
>
|
||||
<span class={styles.uploadIcon}>+</span>
|
||||
<span class={styles.uploadHint}>点击上传</span>
|
||||
<span class={styles.uploadTip}>
|
||||
建议尺寸 1053*351 像素
|
||||
<br />
|
||||
支持 JPG/PNG,最大 10MB
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div class={styles.previewWrapper}>
|
||||
<Spin spinning={formData.imageStatus === 'uploading'} tip="上传中...">
|
||||
<img class={styles.previewImg} src={formData.imageUrl} alt="banner preview" />
|
||||
</Spin>
|
||||
<div class={styles.uploadAgainBtn} onClick={handleRemoveImage}>
|
||||
重新上传
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 跳转类型 ===== */}
|
||||
<Form.Item
|
||||
label="跳转类型"
|
||||
name="jumpType"
|
||||
rules={[{ required: true, message: '请选择跳转类型' }]}
|
||||
>
|
||||
<Select
|
||||
options={JUMP_TYPE_OPTIONS as any}
|
||||
placeholder="请选择"
|
||||
v-model:value={formData.jumpType}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 联动字段:根据跳转类型动态展示 ===== */}
|
||||
{formData.jumpType === 'event_detail' && (
|
||||
<Form.Item
|
||||
label="关联赛事"
|
||||
name="relatedEventId"
|
||||
rules={[{ required: true, message: '请选择关联赛事' }]}
|
||||
>
|
||||
<Select
|
||||
options={EVENT_OPTIONS as any}
|
||||
placeholder="不关联"
|
||||
allowClear
|
||||
v-model:value={formData.relatedEventId}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{formData.jumpType === 'miniprogram_page' && (
|
||||
<Form.Item
|
||||
label="页面路径"
|
||||
name="pagePath"
|
||||
rules={[
|
||||
{ required: true, message: '请输入页面路径' },
|
||||
{
|
||||
pattern: /^\//,
|
||||
message: '页面路径需以 / 开头',
|
||||
},
|
||||
{ max: PATH_MAX, message: `路径最多 ${PATH_MAX} 个字符` },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="/pages/index/index"
|
||||
maxlength={PATH_MAX}
|
||||
allowClear
|
||||
v-model:value={formData.pagePath}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{formData.jumpType === 'custom_link' && (
|
||||
<Form.Item
|
||||
label="自定义链接"
|
||||
name="customUrl"
|
||||
rules={[
|
||||
{ required: true, message: '请输入自定义链接' },
|
||||
{
|
||||
pattern: /^https?:\/\//i,
|
||||
message: '请输入以 http/https 开头的链接',
|
||||
},
|
||||
{ max: URL_MAX, message: `链接最多 ${URL_MAX} 个字符` },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="https://xxx"
|
||||
maxlength={URL_MAX}
|
||||
allowClear
|
||||
v-model:value={formData.customUrl}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{formData.jumpType === 'none' && (
|
||||
<div class={styles.tip}>选择"无链接"时,点击 Banner 不会跳转。</div>
|
||||
)}
|
||||
|
||||
{/* ===== 所属区域 ===== */}
|
||||
<Form.Item
|
||||
label="所属区域"
|
||||
name="region"
|
||||
rules={[{ required: true, message: '请选择所属区域' }]}
|
||||
>
|
||||
<Select
|
||||
options={REGION_OPTIONS as any}
|
||||
placeholder="首页"
|
||||
v-model:value={formData.region}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* ===== 展示时间(开始/结束) ===== */}
|
||||
<div class={styles.timeRow}>
|
||||
<Form.Item
|
||||
label="开始时间"
|
||||
name="displayStartTime"
|
||||
class={styles.timeItem}
|
||||
rules={[{ required: true, message: '请选择开始时间' }]}
|
||||
>
|
||||
<DatePicker
|
||||
placeholder="年-月-日"
|
||||
format="YYYY-MM-DD"
|
||||
value={
|
||||
formData.displayTimeRange?.[0] ? dayjs(formData.displayTimeRange[0]) : undefined
|
||||
}
|
||||
onUpdate:value={(v: any) => {
|
||||
formData.displayTimeRange = [
|
||||
v ? v.format('YYYY-MM-DD') : '',
|
||||
formData.displayTimeRange?.[1] || '',
|
||||
];
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="结束时间"
|
||||
name="displayEndTime"
|
||||
class={styles.timeItem}
|
||||
rules={[
|
||||
{ required: true, message: '请选择结束时间' },
|
||||
{
|
||||
validator: async () => {
|
||||
const [s, e] = formData.displayTimeRange || [];
|
||||
if (s && e && new Date(e) < new Date(s)) {
|
||||
return Promise.reject(new Error('结束时间不能早于开始时间'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker
|
||||
placeholder="年-月-日"
|
||||
format="YYYY-MM-DD"
|
||||
value={
|
||||
formData.displayTimeRange?.[1] ? dayjs(formData.displayTimeRange[1]) : undefined
|
||||
}
|
||||
onUpdate:value={(v: any) => {
|
||||
formData.displayTimeRange = [
|
||||
formData.displayTimeRange?.[0] || '',
|
||||
v ? v.format('YYYY-MM-DD') : '',
|
||||
];
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* ===== 排序 ===== */}
|
||||
<Form.Item
|
||||
label="排序"
|
||||
name="sort"
|
||||
rules={[
|
||||
{ required: true, message: '请输入排序' },
|
||||
{ type: 'number', min: 1, max: 999, message: '排序范围 1-999' },
|
||||
]}
|
||||
extra="数字越小越靠前"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="数字越小越靠前"
|
||||
min={1}
|
||||
max={999}
|
||||
precision={0}
|
||||
style={{ width: '200px' }}
|
||||
v-model:value={formData.sort}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* ===== 底部按钮 ===== */}
|
||||
<div class={styles.footer}>
|
||||
<Button onClick={props.onClose}>取消</Button>
|
||||
<Button type="primary" loading={props.submitting} onClick={handleSubmit}>
|
||||
确认保存
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* ===== 排序 ===== */}
|
||||
<Form.Item
|
||||
label="排序"
|
||||
name="sort"
|
||||
rules={[
|
||||
{ required: true, message: '请输入排序' },
|
||||
{ type: 'number', min: 1, max: 999, message: '排序范围 1-999' },
|
||||
]}
|
||||
extra="数字越小越靠前"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder="数字越小越靠前"
|
||||
min={1}
|
||||
max={999}
|
||||
precision={0}
|
||||
style={{ width: '200px' }}
|
||||
v-model:value={formData.sort}
|
||||
{/* ===== 图片裁剪弹窗 ===== */}
|
||||
<Modal
|
||||
visible={cropperVisible.value}
|
||||
title="裁剪图片"
|
||||
width={800}
|
||||
footer={null}
|
||||
onCancel={onCropperCancel}
|
||||
centered
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ height: '500px' }}>
|
||||
<ImageCropper
|
||||
src={uploadImageUrl.value}
|
||||
aspectRatio={CROP_ASPECT_RATIO}
|
||||
autoCrop={true}
|
||||
autoCropArea={0.8}
|
||||
viewMode={1}
|
||||
dragMode="move"
|
||||
movable={true}
|
||||
scalable={true}
|
||||
zoomable={true}
|
||||
cropBoxMovable={true}
|
||||
cropBoxResizable={true}
|
||||
onCrop={handleCropConfirm}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* ===== 底部按钮 ===== */}
|
||||
<div class={styles.footer}>
|
||||
<Button onClick={props.onClose}>取消</Button>
|
||||
<Button type="primary" loading={props.submitting} onClick={handleSubmit}>
|
||||
确认保存
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ export default defineComponent({
|
||||
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
|
||||
<Form.Item label="标题" name="searchTitle">
|
||||
<Input
|
||||
placeholder="请输入 Banner 标题"
|
||||
placeholder="请输入"
|
||||
style={{ width: '200px' }}
|
||||
allowClear
|
||||
onPressEnter={handleSearch}
|
||||
|
||||
Reference in New Issue
Block a user