Skip to content

Commit 9304b3e

Browse files
committed
update EditorView and HomeView
1 parent ceb1780 commit 9304b3e

3 files changed

Lines changed: 275 additions & 262 deletions

File tree

Lines changed: 109 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,167 +1,178 @@
11
<template>
2-
<div class="image-uploader">
3-
<!-- 预览 -->
4-
<div v-if="imageUrl" class="mb-3">
2+
<div class="w-full">
3+
<label v-if="label" class="block text-xs font-mono font-bold text-gray-500 dark:text-gray-400 uppercase mb-2">
4+
{{ label }}
5+
</label>
6+
7+
<div v-if="previewUrl" class="relative group rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700 shadow-sm aspect-video bg-gray-50 dark:bg-gray-800">
58
<img
6-
:src="imageUrl"
7-
:alt="label"
8-
class="max-h-48 rounded-lg border shadow-sm object-cover"
9+
:src="previewUrl"
10+
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
911
/>
10-
<button
11-
@click="removeImage"
12-
class="mt-2 text-red-600 text-sm hover:text-red-800"
13-
>
14-
删除图片
15-
</button>
12+
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
13+
<button
14+
@click.prevent="removeImage"
15+
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-bold font-mono transition-colors shadow-lg transform hover:scale-105"
16+
>
17+
DELETE
18+
</button>
19+
</div>
1620
</div>
1721

18-
<!-- 上传 -->
1922
<div
20-
v-if="!imageUrl || allowReplace"
21-
class="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center hover:border-blue-500 transition-colors"
22-
:class="{ 'cursor-pointer': !uploading }"
23-
@click="!uploading && triggerFileInput()"
23+
v-else
24+
class="relative border-2 border-dashed rounded-xl p-8 text-center transition-all duration-300"
25+
:class="[
26+
dragover
27+
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/10'
28+
: 'border-gray-300 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-500 hover:bg-gray-50 dark:hover:bg-gray-800/50'
29+
]"
2430
@dragover.prevent="dragover = true"
2531
@dragleave.prevent="dragover = false"
2632
@drop.prevent="onDrop"
27-
:style="dropzoneStyle"
33+
@click="triggerFileInput"
2834
>
2935
<input
3036
type="file"
31-
ref="fileInput"
37+
ref="fileInputRef"
3238
@change="onFileChange"
3339
class="hidden"
34-
accept="image/*"
40+
accept="image/png,image/jpeg,image/gif,image/webp"
3541
/>
3642

37-
<div v-if="uploading">
38-
<div
39-
class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500 mx-auto mb-2"
40-
></div>
41-
<p class="text-gray-600">上传中...</p>
43+
<div v-if="uploading" class="flex flex-col items-center justify-center py-4">
44+
<svg class="animate-spin h-8 w-8 text-blue-500 mb-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
45+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
46+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
47+
</svg>
48+
<span class="text-xs font-mono text-blue-500 animate-pulse">UPLOADING...</span>
4249
</div>
4350

44-
<div v-else>
45-
<svg
46-
xmlns="http://www.w3.org/2000/svg"
47-
class="h-12 w-12 mx-auto text-gray-400 mb-2"
48-
fill="none"
49-
viewBox="0 0 24 24"
50-
stroke="currentColor"
51-
>
52-
<path
53-
stroke-linecap="round"
54-
stroke-linejoin="round"
55-
stroke-width="2"
56-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
57-
/>
51+
<div v-else class="flex flex-col items-center justify-center cursor-pointer py-2">
52+
<svg class="w-10 h-10 text-gray-400 dark:text-gray-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
5854
</svg>
59-
<p class="text-gray-600 mb-1">
60-
{{ label || "点击或拖放图片到此处上传" }}
55+
<p class="text-sm font-bold text-gray-600 dark:text-gray-300">
56+
Click or Drag to Upload
57+
</p>
58+
<p class="text-xs text-gray-400 mt-1 font-mono">
59+
PNG, JPG, GIF up to 5MB
6160
</p>
62-
<p class="text-gray-500 text-xs">支持 JPG、PNG、GIF,最大 5 MB</p>
6361
</div>
6462
</div>
6563

66-
<!-- 错误 -->
67-
<div v-if="error" class="mt-2 text-red-600 text-sm">{{ error }}</div>
64+
<div v-if="error" class="mt-2 text-xs font-mono text-red-500 flex items-center">
65+
<span class="mr-1">⚠</span> {{ error }}
66+
</div>
6867
</div>
6968
</template>
7069

7170
<script setup>
72-
import { ref, computed } from "vue";
71+
import { ref, watch, onMounted } from "vue";
7372
import api from "../api";
7473
74+
// Vue 3 标准 v-model 是 'modelValue'
7575
const props = defineProps({
76-
value: String, // v-model 绑定到相对路径
76+
modelValue: String,
7777
label: String,
78-
uploadType: {
79-
type: String,
80-
default: "post", // 'avatar' | 'post'
81-
},
82-
allowReplace: { type: Boolean, default: true },
83-
postId: { type: [Number, String], default: null },
78+
uploadType: { type: String, default: "image" } // 'image' (generic) | 'avatar'
8479
});
8580
86-
const emit = defineEmits(["update:value", "upload-success", "upload-error"]);
81+
const emit = defineEmits(["update:modelValue", "upload-success", "upload-error"]);
8782
88-
// state
89-
const imageUrl = ref(""); // 直接存绝对 URL,方便预览
83+
const fileInputRef = ref(null);
9084
const uploading = ref(false);
91-
const error = ref("");
92-
const fileInput = ref(null);
9385
const dragover = ref(false);
86+
const error = ref("");
87+
const previewUrl = ref("");
88+
89+
// 初始化回显逻辑
90+
onMounted(() => {
91+
if (props.modelValue) {
92+
// 如果是完整 URL 直接用,如果是相对路径则拼上前缀
93+
const isAbsolute = props.modelValue.startsWith('http');
94+
// 注意:这里需要根据你的环境变量调整,如果没有定义 VITE_API_URL,默认用空或后端地址
95+
const baseUrl = import.meta.env.VITE_API_URL || '';
96+
previewUrl.value = isAbsolute ? props.modelValue : `${baseUrl}${props.modelValue}`;
97+
}
98+
});
9499
95-
// 初始值(父组件可能传入已保存的相对路径)
96-
if (props.value) {
97-
const base = import.meta.env.VITE_API_URL || window.location.origin;
98-
imageUrl.value = props.value.startsWith("http")
99-
? props.value
100-
: base + props.value;
101-
}
102-
103-
// style
104-
const dropzoneStyle = computed(() =>
105-
dragover.value
106-
? "border-color:#3b82f6;background-color:rgba(59,130,246,.05)"
107-
: ""
108-
);
100+
// 监听外部 modelValue 变化(例如重置表单时)
101+
watch(() => props.modelValue, (newVal) => {
102+
if (!newVal) {
103+
previewUrl.value = "";
104+
} else if (newVal !== previewUrl.value) {
105+
// 只有当新值和当前预览不一致时才更新(避免循环)
106+
const isAbsolute = newVal.startsWith('http');
107+
const baseUrl = import.meta.env.VITE_API_URL || '';
108+
previewUrl.value = isAbsolute ? newVal : `${baseUrl}${newVal}`;
109+
}
110+
});
109111
110-
// helpers
111-
const triggerFileInput = () => fileInput.value.click();
112+
const triggerFileInput = () => fileInputRef.value.click();
112113
113114
const onFileChange = (e) => {
114115
const file = e.target.files[0];
115-
if (file) uploadImage(file);
116-
e.target.value = null; // 允许重复选同一文件
116+
if (file) handleUpload(file);
117+
e.target.value = null; // Reset input
117118
};
118119
119120
const onDrop = (e) => {
120121
dragover.value = false;
121122
const file = e.dataTransfer.files[0];
122-
if (!file || !file.type.startsWith("image/"))
123-
return (error.value = "请上传有效图片");
124-
uploadImage(file);
123+
if (file) handleUpload(file);
125124
};
126125
127-
async function uploadImage(file) {
128-
// 校验
129-
if (file.size > 5 * 1024 * 1024) return (error.value = "图片最大 5 MB");
126+
const handleUpload = async (file) => {
127+
if (!file.type.startsWith("image/")) {
128+
error.value = "File must be an image.";
129+
return;
130+
}
131+
if (file.size > 5 * 1024 * 1024) {
132+
error.value = "File size exceeds 5MB.";
133+
return;
134+
}
135+
130136
error.value = "";
131137
uploading.value = true;
132138
133139
try {
134140
const fd = new FormData();
135-
props.uploadType === "avatar"
136-
? fd.append("avatar", file)
137-
: fd.append("image", file);
141+
// 根据你的 API 定义调整字段名
142+
const fieldName = props.uploadType === 'avatar' ? 'avatar' : 'image';
143+
fd.append(fieldName, file);
138144
139145
let res;
140-
if (props.uploadType === "avatar") {
146+
if (props.uploadType === 'avatar') {
141147
res = await api.uploadAvatar(fd);
142-
} else if (props.postId) {
143-
res = await api.uploadPostCover(props.postId, fd);
144148
} else {
149+
// 默认使用通用的图片上传,返回 { url, path }
145150
res = await api.uploadPostImage(fd);
146151
}
147152
148-
// 后端统一 {url, path}
149-
const { url, path } = res;
153+
// 解析返回值:你的 API 返回 { url, path }
154+
// url: 绝对路径 (用于预览)
155+
// path: 相对路径 (用于存库)
156+
const { url, path } = res;
150157
151-
imageUrl.value = url; // 绝对 URL → 预览
152-
emit("update:value", path); // 相对路径 → 存库
158+
// 更新预览
159+
previewUrl.value = url;
160+
161+
// 向父组件更新值 (通常存 path 到数据库比较灵活)
162+
emit("update:modelValue", path);
153163
emit("upload-success", url);
164+
154165
} catch (err) {
155166
console.error(err);
156-
error.value = "上传失败,请重试";
167+
error.value = "Upload failed. Please try again.";
157168
emit("upload-error", err);
158169
} finally {
159170
uploading.value = false;
160171
}
161-
}
172+
};
162173
163-
function removeImage() {
164-
imageUrl.value = "";
165-
emit("update:value", "");
166-
}
167-
</script>
174+
const removeImage = () => {
175+
previewUrl.value = "";
176+
emit("update:modelValue", "");
177+
};
178+
</script>

0 commit comments

Comments
 (0)