zhuli 1 тиждень тому
батько
коміт
95507e05a3

+ 71 - 0
src/api/modules/dynamicManagement.ts

@@ -0,0 +1,71 @@
+import { PORT_NONE } from "@/api/config/servicePort";
+import http from "@/api";
+
+/**
+ * 动态管理相关接口
+ */
+
+// 获取动态列表
+export const getDynamicList = (params: {
+  type: string; // recommend 推荐, follow 关注
+  page: number;
+  pageSize: number;
+}) => {
+  return http.get(PORT_NONE + `/dynamic/getDynamicList`, params);
+};
+
+// 发布动态
+export const publishDynamic = (params: { title: string; content: string; imageUrl: string }) => {
+  return http.post(PORT_NONE + `/dynamic/publishDynamic`, params);
+};
+
+// 删除动态
+export const deleteDynamic = (params: { id: number }) => {
+  return http.post(PORT_NONE + `/dynamic/deleteDynamic`, params);
+};
+
+// 点赞动态
+export const likeDynamic = (params: { id: number }) => {
+  return http.post(PORT_NONE + `/dynamic/likeDynamic`, params);
+};
+
+// 取消点赞
+export const unlikeDynamic = (params: { id: number }) => {
+  return http.post(PORT_NONE + `/dynamic/unlikeDynamic`, params);
+};
+
+// 获取动态详情
+export const getDynamicDetail = (params: { id: number }) => {
+  return http.get(PORT_NONE + `/dynamic/getDynamicDetail`, params);
+};
+
+// 上传图片
+export const uploadDynamicImage = (params: FormData) => {
+  return http.post(PORT_NONE + `/dynamic/uploadImage`, params);
+};
+
+// 保存草稿
+export const saveDraft = (params: {
+  title: string;
+  content: string;
+  images: string[];
+  location?: string;
+  locationId?: string;
+}) => {
+  return http.post(PORT_NONE + `/dynamic/saveDraft`, params);
+};
+
+// 获取草稿列表
+export const getDraftList = (params: { page: number; pageSize: number }) => {
+  return http.get(PORT_NONE + `/dynamic/getDraftList`, params);
+};
+
+// 删除草稿
+export const deleteDraft = (params: { id: number }) => {
+  return http.post(PORT_NONE + `/dynamic/deleteDraft`, params);
+};
+
+// 获取位置列表
+export const getLocationList = (params: { keyword?: string }) => {
+  return http.get(PORT_NONE + `/dynamic/getLocationList`, params);
+};

+ 28 - 0
src/assets/json/authMenuList.json

@@ -658,6 +658,34 @@
           }
         }
       ]
+    },
+    {
+      "path": "/dynamicManagement",
+      "name": "dynamicManagement",
+      "redirect": "/dynamicManagement/index",
+      "meta": {
+        "icon": "Operation",
+        "title": "动态",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [{
+        "path": "/dynamicManagement/index",
+        "name": "dynamicManagement",
+        "component": "/dynamicManagement/index",
+        "meta": {
+          "icon": "Setting",
+          "title": "动态广场",
+          "isLink": "",
+          "isHide": false,
+          "isFull": false,
+          "isAffix": false,
+          "isKeepAlive": false
+        }
+      }]
     }
   ],
   "msg": "成功"

+ 553 - 0
src/views/dynamicManagement/index.vue

@@ -0,0 +1,553 @@
+<template>
+  <div class="dynamic-management-container">
+    <!-- 头部:Tabs和发布按钮 -->
+    <div class="header-section">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane label="推荐" name="recommend" />
+        <el-tab-pane label="关注" name="follow" />
+      </el-tabs>
+      <div class="action-buttons">
+        <el-button type="primary" @click="handlePublish"> 发布动态 </el-button>
+      </div>
+    </div>
+
+    <!-- 内容区域 -->
+    <div v-if="dynamicList.length > 0" class="content-section">
+      <!-- 动态卡片网格 -->
+      <div class="dynamic-grid">
+        <div v-for="item in paginatedList" :key="item.id" class="dynamic-card" @click="handleCardClick(item)">
+          <!-- 图片区域 -->
+          <div class="dynamic-image-wrapper">
+            <img v-if="item.imageUrl" :src="item.imageUrl" :alt="item.title" class="dynamic-image" />
+            <div v-else class="image-placeholder">
+              <el-icon :size="48" color="#999">
+                <Picture />
+              </el-icon>
+            </div>
+          </div>
+
+          <!-- 动态内容 -->
+          <div class="dynamic-content">
+            <div class="dynamic-text">
+              {{ item.title }}
+            </div>
+
+            <!-- 用户信息和点赞 -->
+            <div class="dynamic-footer">
+              <div class="user-info">
+                <div class="user-avatar">
+                  <img v-if="item.userAvatar" :src="item.userAvatar" :alt="item.userName" />
+                  <el-icon v-else :size="24">
+                    <Avatar />
+                  </el-icon>
+                </div>
+                <span class="user-name">{{ item.userName }}</span>
+              </div>
+
+              <div class="like-section" @click.stop="handleLike(item)">
+                <el-icon :size="18" :color="item.isLiked ? '#f56c6c' : '#999'" class="like-icon">
+                  <Star />
+                </el-icon>
+                <span class="like-count">{{ item.likeCount }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <div v-else class="empty-section">
+      <el-empty description="暂无动态数据" />
+    </div>
+
+    <!-- 分页 -->
+    <div v-if="dynamicList.length > 0" class="pagination-section">
+      <el-pagination
+        v-model:current-page="pagination.page"
+        v-model:page-size="pagination.pageSize"
+        :page-sizes="[10, 20, 30, 50]"
+        :total="pagination.total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 发布动态对话框 -->
+    <el-dialog v-model="publishDialogVisible" title="发布动态" width="600px" @close="handleCloseDialog">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
+        <el-form-item label="动态标题" prop="title">
+          <el-input v-model="formData.title" placeholder="请输入动态标题" maxlength="50" show-word-limit />
+        </el-form-item>
+
+        <el-form-item label="动态内容" prop="content">
+          <el-input
+            v-model="formData.content"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入动态内容"
+            maxlength="200"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="上传图片" prop="imageUrl">
+          <el-upload
+            v-model:file-list="fileList"
+            list-type="picture-card"
+            :limit="1"
+            :on-preview="handlePicturePreview"
+            :on-remove="handleRemoveImage"
+            :before-upload="beforeImageUpload"
+            :http-request="handleImageUpload"
+            accept="image/*"
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="publishDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="handleSubmit">发布</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览对话框 -->
+    <el-dialog v-model="previewDialogVisible" width="800px">
+      <img :src="previewImageUrl" alt="预览图片" style="width: 100%" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import { Picture, Avatar, Star, Plus } from "@element-plus/icons-vue";
+import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
+// import { getDynamicList, publishDynamic, uploadDynamicImage } from "@/api/modules/dynamicManagement";
+
+// 接口定义
+interface DynamicItem {
+  id: number;
+  title: string;
+  content: string;
+  imageUrl: string;
+  userName: string;
+  userAvatar: string;
+  likeCount: number;
+  isLiked: boolean;
+  createTime: string;
+}
+
+interface FormData {
+  title: string;
+  content: string;
+  imageUrl: string;
+}
+
+// 响应式数据
+const activeTab = ref("recommend");
+const dynamicList = ref<DynamicItem[]>([]);
+const publishDialogVisible = ref(false);
+const previewDialogVisible = ref(false);
+const previewImageUrl = ref("");
+const fileList = ref<UploadUserFile[]>([]);
+const formRef = ref<FormInstance>();
+
+// 分页
+const pagination = reactive({
+  page: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 表单数据
+const formData = reactive<FormData>({
+  title: "",
+  content: "",
+  imageUrl: ""
+});
+
+// 表单验证规则
+const rules = reactive<FormRules<FormData>>({
+  title: [{ required: true, message: "请输入动态标题", trigger: "blur" }],
+  content: [{ required: true, message: "请输入动态内容", trigger: "blur" }]
+});
+
+// 计算分页后的列表
+const paginatedList = computed(() => {
+  const start = (pagination.page - 1) * pagination.pageSize;
+  const end = start + pagination.pageSize;
+  return dynamicList.value.slice(start, end);
+});
+
+// 标签切换
+const handleTabClick = () => {
+  pagination.page = 1;
+  loadDynamicList();
+};
+
+// 发布动态
+const handlePublish = () => {
+  publishDialogVisible.value = true;
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pagination.pageSize = val;
+  pagination.page = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  pagination.page = val;
+};
+
+// 图片预览
+const handlePicturePreview = (uploadFile: UploadUserFile) => {
+  previewImageUrl.value = uploadFile.url!;
+  previewDialogVisible.value = true;
+};
+
+// 移除图片
+const handleRemoveImage = () => {
+  formData.imageUrl = "";
+  fileList.value = [];
+};
+
+// 图片上传前验证
+const beforeImageUpload = (file: File) => {
+  const isImage = file.type.startsWith("image/");
+  const isLt5M = file.size / 1024 / 1024 < 5;
+
+  if (!isImage) {
+    ElMessage.error("只能上传图片文件!");
+    return false;
+  }
+  if (!isLt5M) {
+    ElMessage.error("图片大小不能超过 5MB!");
+    return false;
+  }
+  return true;
+};
+
+// 自定义上传
+const handleImageUpload = async (options: any) => {
+  const { file } = options;
+
+  try {
+    // TODO: 集成真实上传接口时,取消下面的注释
+    // const uploadFormData = new FormData();
+    // uploadFormData.append('file', file);
+    // const res = await uploadDynamicImage(uploadFormData);
+    // formData.imageUrl = res.data.url;
+    // ElMessage.success('图片上传成功');
+
+    // 临时方案:使用 FileReader 模拟上传
+    const reader = new FileReader();
+    reader.onload = e => {
+      formData.imageUrl = e.target?.result as string;
+      ElMessage.success("图片上传成功");
+    };
+    reader.readAsDataURL(file);
+  } catch (error) {
+    ElMessage.error("图片上传失败");
+  }
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  await formRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      try {
+        // TODO: 集成真实接口时,取消下面的注释
+        // await publishDynamic({
+        //   title: formData.title,
+        //   content: formData.content,
+        //   imageUrl: formData.imageUrl
+        // });
+
+        // 临时方案:模拟添加到列表
+        const newDynamic: DynamicItem = {
+          id: Date.now(),
+          title: formData.title,
+          content: formData.content,
+          imageUrl: formData.imageUrl,
+          userName: "甜品店",
+          userAvatar: "",
+          likeCount: 0,
+          isLiked: false,
+          createTime: new Date().toISOString()
+        };
+
+        dynamicList.value.unshift(newDynamic);
+        pagination.total = dynamicList.value.length;
+
+        ElMessage.success("发布成功");
+        publishDialogVisible.value = false;
+        handleCloseDialog();
+      } catch (error) {
+        ElMessage.error("发布失败");
+      }
+    }
+  });
+};
+
+// 关闭对话框
+const handleCloseDialog = () => {
+  formRef.value?.resetFields();
+  fileList.value = [];
+  formData.imageUrl = "";
+};
+
+// 加载动态列表
+const loadDynamicList = async () => {
+  try {
+    // TODO: 集成真实接口时,取消下面的注释
+    // const res = await getDynamicList({
+    //   type: activeTab.value,
+    //   page: pagination.page,
+    //   pageSize: pagination.pageSize
+    // });
+    // dynamicList.value = res.data.list;
+    // pagination.total = res.data.total;
+
+    // 临时方案:使用模拟数据
+    const mockData: DynamicItem[] = Array.from({ length: 30 }, (_, i) => ({
+      id: i + 1,
+      title: "这家店超好吃....",
+      content: "非常好吃的甜品店,环境优雅,服务态度好,推荐大家来尝试!",
+      imageUrl: "",
+      userName: "甜品店",
+      userAvatar: "",
+      likeCount: 20,
+      isLiked: false,
+      createTime: new Date().toISOString()
+    }));
+
+    dynamicList.value = mockData;
+    pagination.total = mockData.length;
+  } catch (error) {
+    ElMessage.error("加载动态列表失败");
+  }
+};
+
+// 点击卡片
+const handleCardClick = (item: DynamicItem) => {
+  console.log("查看动态详情", item);
+  // TODO: 可以添加查看详情的逻辑
+  ElMessage.info(`查看动态: ${item.title}`);
+};
+
+// 点赞/取消点赞
+const handleLike = async (item: DynamicItem) => {
+  try {
+    // TODO: 集成真实接口时,取消下面的注释
+    // if (item.isLiked) {
+    //   await unlikeDynamic({ id: item.id });
+    // } else {
+    //   await likeDynamic({ id: item.id });
+    // }
+
+    // 临时方案:直接修改状态
+    item.isLiked = !item.isLiked;
+    item.likeCount += item.isLiked ? 1 : -1;
+
+    ElMessage.success(item.isLiked ? "点赞成功" : "取消点赞");
+  } catch (error) {
+    ElMessage.error("操作失败");
+  }
+};
+
+// 初始化
+onMounted(() => {
+  loadDynamicList();
+});
+</script>
+
+<style scoped lang="scss">
+.dynamic-management-container {
+  min-height: calc(100vh - 120px);
+  padding: 20px;
+  background: #ffffff;
+
+  // 头部区域
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #e4e7ed;
+    :deep(.el-tabs) {
+      flex: 1;
+      margin-bottom: -1px;
+      .el-tabs__header {
+        margin-bottom: 0;
+      }
+      .el-tabs__nav-wrap::after {
+        display: none;
+      }
+    }
+    .action-buttons {
+      padding-bottom: 10px;
+      margin-left: 20px;
+    }
+  }
+
+  // 内容区域
+  .content-section {
+    margin-top: 20px;
+  }
+
+  // 动态网格布局
+  .dynamic-grid {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 20px;
+    margin-bottom: 20px;
+
+    @media (width <= 1400px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+
+    @media (width <= 768px) {
+      grid-template-columns: repeat(1, 1fr);
+    }
+  }
+
+  // 动态卡片
+  .dynamic-card {
+    overflow: hidden;
+    cursor: pointer;
+    background: #ffffff;
+    border: 1px solid #e4e7ed;
+    border-radius: 8px;
+    transition: all 0.3s;
+    &:hover {
+      box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+      transform: translateY(-2px);
+    }
+
+    // 图片区域
+    .dynamic-image-wrapper {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 220px;
+      overflow: hidden;
+      background: #f5f7fa;
+      .dynamic-image {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+      .image-placeholder {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        background: #f5f7fa;
+      }
+    }
+
+    // 动态内容
+    .dynamic-content {
+      padding: 12px 16px;
+      .dynamic-text {
+        margin-bottom: 12px;
+        overflow: hidden;
+        font-size: 14px;
+        color: #333333;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      // 底部信息
+      .dynamic-footer {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        .user-info {
+          display: flex;
+          gap: 8px;
+          align-items: center;
+          .user-avatar {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 24px;
+            height: 24px;
+            overflow: hidden;
+            background: #e4e7ed;
+            border-radius: 50%;
+            img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+          }
+          .user-name {
+            font-size: 13px;
+            color: #666666;
+          }
+        }
+        .like-section {
+          display: flex;
+          gap: 4px;
+          align-items: center;
+          .like-icon {
+            cursor: pointer;
+            transition: color 0.3s;
+            &:hover {
+              color: #f56c6c;
+            }
+          }
+          .like-count {
+            font-size: 13px;
+            color: #666666;
+          }
+        }
+      }
+    }
+  }
+
+  // 空状态
+  .empty-section {
+    padding: 80px 0;
+    text-align: center;
+  }
+
+  // 分页
+  .pagination-section {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+    margin-top: 30px;
+  }
+
+  // 对话框
+  :deep(.el-dialog) {
+    .el-upload--picture-card {
+      width: 148px;
+      height: 148px;
+    }
+    .el-upload-list--picture-card .el-upload-list__item {
+      width: 148px;
+      height: 148px;
+    }
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+  }
+}
+</style>

+ 634 - 0
src/views/dynamicManagement/publishDynamic.vue

@@ -0,0 +1,634 @@
+<template>
+  <div class="publish-dynamic-container">
+    <!-- 头部导航 -->
+    <div class="header-bar">
+      <div class="header-left">
+        <el-button text @click="handleGoBack">
+          <el-icon :size="20">
+            <ArrowLeft />
+          </el-icon>
+          返回
+        </el-button>
+      </div>
+      <div class="header-title">发布动态</div>
+      <div class="header-right" />
+    </div>
+
+    <!-- 表单内容 -->
+    <div class="form-container">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-position="top">
+        <!-- 图片上传 -->
+        <el-form-item label="图片" prop="images">
+          <div class="upload-section">
+            <el-upload
+              v-model:file-list="fileList"
+              list-type="picture-card"
+              :limit="20"
+              :on-preview="handlePicturePreview"
+              :on-remove="handleRemoveImage"
+              :on-change="handleFileChange"
+              :before-upload="beforeImageUpload"
+              :http-request="handleImageUpload"
+              accept="image/*"
+              multiple
+              class="dynamic-upload"
+            >
+              <div class="upload-trigger">
+                <el-icon :size="32" color="#999">
+                  <Plus />
+                </el-icon>
+                <div class="upload-count">({{ fileList.length }}/20)</div>
+              </div>
+            </el-upload>
+          </div>
+        </el-form-item>
+
+        <!-- 标题 -->
+        <el-form-item label="标题" prop="title">
+          <el-input v-model="formData.title" placeholder="请输入标题" maxlength="50" show-word-limit size="large" />
+        </el-form-item>
+
+        <!-- 正文 -->
+        <el-form-item label="正文" prop="content">
+          <el-input
+            v-model="formData.content"
+            type="textarea"
+            placeholder="请输入正文"
+            :rows="8"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <!-- 位置 -->
+        <el-form-item label="位置" prop="location">
+          <el-input v-model="formData.location" placeholder="请选择或许位置" size="large" readonly @click="handleSelectLocation">
+            <template #suffix>
+              <el-icon color="#999">
+                <Location />
+              </el-icon>
+            </template>
+          </el-input>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 底部操作按钮 -->
+    <div class="footer-actions">
+      <el-button size="large" class="draft-btn" @click="handleSaveDraft"> 保存草稿 </el-button>
+      <el-button type="primary" size="large" class="publish-btn" :loading="publishing" @click="handlePublish"> 发布 </el-button>
+    </div>
+
+    <!-- 图片预览对话框 -->
+    <el-dialog v-model="previewDialogVisible" width="800px" append-to-body>
+      <img :src="previewImageUrl" alt="预览图片" style="width: 100%" />
+    </el-dialog>
+
+    <!-- 位置选择对话框 -->
+    <el-dialog v-model="locationDialogVisible" title="选择位置" width="500px" append-to-body>
+      <el-input v-model="locationSearch" placeholder="搜索位置" clearable @input="handleSearchLocation">
+        <template #prefix>
+          <el-icon>
+            <Search />
+          </el-icon>
+        </template>
+      </el-input>
+      <div class="location-list">
+        <div v-for="location in locationList" :key="location.id" class="location-item" @click="handleChooseLocation(location)">
+          <el-icon>
+            <Location />
+          </el-icon>
+          <span>{{ location.name }}</span>
+        </div>
+        <el-empty v-if="locationList.length === 0" description="暂无位置信息" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { ArrowLeft, Plus, Location, Search } from "@element-plus/icons-vue";
+import type { FormInstance, FormRules, UploadUserFile, UploadFile } from "element-plus";
+// import { publishDynamic, saveDraft, uploadDynamicImage } from "@/api/modules/dynamicManagement";
+
+const router = useRouter();
+
+// 接口定义
+interface FormData {
+  title: string;
+  content: string;
+  images: string[];
+  location: string;
+  locationId?: string;
+}
+
+interface LocationItem {
+  id: string;
+  name: string;
+  address: string;
+}
+
+// 响应式数据
+const formRef = ref<FormInstance>();
+const fileList = ref<UploadUserFile[]>([]);
+const previewDialogVisible = ref(false);
+const previewImageUrl = ref("");
+const locationDialogVisible = ref(false);
+const locationSearch = ref("");
+const locationList = ref<LocationItem[]>([]);
+const publishing = ref(false);
+const pendingUploadFiles = ref<UploadFile[]>([]);
+const uploading = ref(false);
+
+// 表单数据
+const formData = reactive<FormData>({
+  title: "",
+  content: "",
+  images: [],
+  location: ""
+});
+
+// 表单验证规则
+const rules = reactive<FormRules<FormData>>({
+  title: [{ required: true, message: "请输入标题", trigger: "blur" }],
+  content: [{ required: true, message: "请输入正文", trigger: "blur" }]
+});
+
+// 返回上一页
+const handleGoBack = async () => {
+  // 检查是否有未保存的内容
+  if (formData.title || formData.content || fileList.value.length > 0) {
+    try {
+      await ElMessageBox.confirm("当前有未保存的内容,确定要离开吗?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      });
+      router.back();
+    } catch {
+      // 用户取消
+    }
+  } else {
+    router.back();
+  }
+};
+
+// 文件改变
+const handleFileChange = (uploadFile: UploadFile, uploadFiles: UploadFile[]) => {
+  // 验证文件
+  if (uploadFile.raw) {
+    const isImage = uploadFile.raw.type.startsWith("image/");
+    if (!isImage) {
+      const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
+      if (index > -1) {
+        uploadFiles.splice(index, 1);
+      }
+      ElMessage.warning("只能上传图片文件");
+      return;
+    }
+
+    const isLt20M = uploadFile.raw.size / 1024 / 1024 < 20;
+    if (!isLt20M) {
+      const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
+      if (index > -1) {
+        uploadFiles.splice(index, 1);
+      }
+      ElMessage.warning("图片大小不能超过 20MB");
+      return;
+    }
+  }
+
+  // 添加到待上传队列
+  const existingIndex = fileList.value.findIndex(f => f.uid === uploadFile.uid);
+  if (existingIndex === -1 && uploadFile.status) {
+    fileList.value.push(uploadFile as UploadUserFile);
+  }
+
+  const readyFiles = fileList.value.filter(file => file.status === "ready");
+  if (readyFiles.length) {
+    readyFiles.forEach(file => {
+      if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
+        pendingUploadFiles.value.push(file as UploadFile);
+      }
+    });
+  }
+  processUploadQueue();
+};
+
+// 处理上传队列
+const processUploadQueue = async () => {
+  if (uploading.value || pendingUploadFiles.value.length === 0) {
+    return;
+  }
+  const file = pendingUploadFiles.value.shift();
+  if (file) {
+    await uploadSingleFile(file);
+    processUploadQueue();
+  }
+};
+
+// 单个文件上传
+const uploadSingleFile = async (file: UploadFile) => {
+  if (!file.raw) {
+    return;
+  }
+
+  const rawFile = file.raw as File;
+  const uploadFormData = new FormData();
+  uploadFormData.append("file", rawFile);
+
+  file.status = "uploading";
+  file.percentage = 0;
+  uploading.value = true;
+
+  try {
+    // TODO: 集成真实上传接口时,取消下面的注释
+    // const result = await uploadDynamicImage(uploadFormData);
+    // if (result?.code === 200 && result.data) {
+    //   const imageUrl = result.data.url;
+    //   file.status = "success";
+    //   file.percentage = 100;
+    //   file.url = imageUrl;
+    //   file.response = { url: imageUrl };
+    //   if (!formData.images.includes(imageUrl)) {
+    //     formData.images.push(imageUrl);
+    //   }
+    // } else {
+    //   throw new Error(result?.msg || "图片上传失败");
+    // }
+
+    // 临时方案:使用 FileReader 模拟上传
+    await new Promise<void>((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = e => {
+        const imageUrl = e.target?.result as string;
+        file.status = "success";
+        file.percentage = 100;
+        file.url = imageUrl;
+        file.response = { url: imageUrl };
+        if (!formData.images.includes(imageUrl)) {
+          formData.images.push(imageUrl);
+        }
+        resolve();
+      };
+      reader.onerror = () => reject(new Error("读取文件失败"));
+      reader.readAsDataURL(rawFile);
+    });
+  } catch (error: any) {
+    file.status = "fail";
+    if (file.url && file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(file.url);
+    }
+    const index = fileList.value.findIndex(f => f.uid === file.uid);
+    if (index > -1) {
+      fileList.value.splice(index, 1);
+    }
+    ElMessage.error(error?.message || "图片上传失败");
+  } finally {
+    uploading.value = false;
+    fileList.value = [...fileList.value];
+  }
+};
+
+// 图片上传前验证
+const beforeImageUpload = (file: File) => {
+  const isImage = file.type.startsWith("image/");
+  const isLt20M = file.size / 1024 / 1024 < 20;
+
+  if (!isImage) {
+    ElMessage.error("只能上传图片文件!");
+    return false;
+  }
+  if (!isLt20M) {
+    ElMessage.error("图片大小不能超过 20MB!");
+    return false;
+  }
+  return true;
+};
+
+// 自定义上传
+const handleImageUpload = async (options: any) => {
+  // 上传逻辑已在 uploadSingleFile 中处理
+  return;
+};
+
+// 图片预览
+const handlePicturePreview = (uploadFile: UploadUserFile) => {
+  previewImageUrl.value = uploadFile.url!;
+  previewDialogVisible.value = true;
+};
+
+// 移除图片
+const handleRemoveImage = (uploadFile: UploadUserFile, uploadFiles: UploadUserFile[]) => {
+  const index = formData.images.findIndex(url => url === uploadFile.url);
+  if (index > -1) {
+    formData.images.splice(index, 1);
+  }
+};
+
+// 选择位置
+const handleSelectLocation = () => {
+  locationDialogVisible.value = true;
+  // 加载位置列表
+  loadLocationList();
+};
+
+// 搜索位置
+const handleSearchLocation = () => {
+  // TODO: 实现位置搜索
+  loadLocationList(locationSearch.value);
+};
+
+// 加载位置列表
+const loadLocationList = (keyword?: string) => {
+  // TODO: 集成真实接口
+  // 模拟数据
+  const mockLocations: LocationItem[] = [
+    { id: "1", name: "天安门广场", address: "北京市东城区" },
+    { id: "2", name: "故宫博物院", address: "北京市东城区景山前街4号" },
+    { id: "3", name: "颐和园", address: "北京市海淀区新建宫门路19号" },
+    { id: "4", name: "长城", address: "北京市延庆区" },
+    { id: "5", name: "鸟巢", address: "北京市朝阳区国家体育场南路1号" }
+  ];
+
+  if (keyword) {
+    locationList.value = mockLocations.filter(loc => loc.name.includes(keyword) || loc.address.includes(keyword));
+  } else {
+    locationList.value = mockLocations;
+  }
+};
+
+// 选择位置
+const handleChooseLocation = (location: LocationItem) => {
+  formData.location = location.name;
+  formData.locationId = location.id;
+  locationDialogVisible.value = false;
+};
+
+// 保存草稿
+const handleSaveDraft = async () => {
+  if (!formData.title && !formData.content && fileList.value.length === 0) {
+    ElMessage.warning("请至少填写标题或正文");
+    return;
+  }
+
+  try {
+    // TODO: 集成真实接口时,取消下面的注释
+    // await saveDraft({
+    //   title: formData.title,
+    //   content: formData.content,
+    //   images: formData.images,
+    //   location: formData.location,
+    //   locationId: formData.locationId
+    // });
+
+    ElMessage.success("草稿保存成功");
+    router.back();
+  } catch (error) {
+    ElMessage.error("保存草稿失败");
+  }
+};
+
+// 发布动态
+const handlePublish = async () => {
+  if (!formRef.value) return;
+
+  await formRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      // 检查是否有图片正在上传
+      if (uploading.value || pendingUploadFiles.value.length > 0) {
+        ElMessage.warning("图片正在上传中,请稍候...");
+        return;
+      }
+
+      publishing.value = true;
+
+      try {
+        // TODO: 集成真实接口时,取消下面的注释
+        // await publishDynamic({
+        //   title: formData.title,
+        //   content: formData.content,
+        //   images: formData.images,
+        //   location: formData.location,
+        //   locationId: formData.locationId
+        // });
+
+        // 模拟发布延迟
+        await new Promise(resolve => setTimeout(resolve, 1000));
+
+        ElMessage.success("发布成功");
+        router.back();
+      } catch (error) {
+        ElMessage.error("发布失败");
+      } finally {
+        publishing.value = false;
+      }
+    }
+  });
+};
+
+// 初始化
+onMounted(() => {
+  // 可以在这里加载草稿数据
+});
+</script>
+
+<style scoped lang="scss">
+.publish-dynamic-container {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background: #f5f7fa;
+
+  // 头部导航
+  .header-bar {
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 60px;
+    padding: 0 20px;
+    background: #ffffff;
+    box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
+    .header-left {
+      flex: 1;
+      :deep(.el-button) {
+        padding: 8px 0;
+        font-size: 16px;
+        color: #606266;
+        &:hover {
+          color: #409eff;
+        }
+        .el-icon {
+          margin-right: 4px;
+        }
+      }
+    }
+    .header-title {
+      flex: 1;
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+      text-align: center;
+    }
+    .header-right {
+      flex: 1;
+    }
+  }
+
+  // 表单容器
+  .form-container {
+    flex: 1;
+    width: 100%;
+    max-width: 800px;
+    padding: 30px 20px 100px;
+    margin: 0 auto;
+    :deep(.el-form) {
+      .el-form-item {
+        margin-bottom: 30px;
+        .el-form-item__label {
+          padding-bottom: 12px;
+          font-size: 16px;
+          font-weight: 600;
+          color: #303133;
+        }
+      }
+    }
+
+    // 图片上传区域
+    .upload-section {
+      :deep(.el-upload-list) {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+      }
+      :deep(.el-upload--picture-card) {
+        width: 148px;
+        height: 148px;
+        border: 2px dashed #dcdfe6;
+        border-radius: 8px;
+        transition: all 0.3s;
+        &:hover {
+          border-color: #409eff;
+        }
+        .upload-trigger {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          justify-content: center;
+          height: 100%;
+          .upload-count {
+            margin-top: 8px;
+            font-size: 14px;
+            color: #909399;
+          }
+        }
+      }
+      :deep(.el-upload-list--picture-card .el-upload-list__item) {
+        width: 148px;
+        height: 148px;
+        margin: 0;
+        border-radius: 8px;
+      }
+    }
+
+    // 输入框样式
+    :deep(.el-input__inner),
+    :deep(.el-textarea__inner) {
+      border-radius: 8px;
+    }
+    :deep(.el-textarea__inner) {
+      padding: 12px 15px;
+      font-size: 15px;
+      line-height: 1.6;
+    }
+  }
+
+  // 底部操作按钮
+  .footer-actions {
+    position: fixed;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 99;
+    display: flex;
+    gap: 16px;
+    justify-content: center;
+    padding: 16px 20px;
+    background: #ffffff;
+    box-shadow: 0 -2px 8px rgb(0 0 0 / 8%);
+    .el-button {
+      min-width: 180px;
+      height: 48px;
+      font-size: 16px;
+      border-radius: 8px;
+    }
+    .draft-btn {
+      color: #606266;
+      background: #f5f7fa;
+      border-color: #dcdfe6;
+      &:hover {
+        color: #409eff;
+        background: #ecf5ff;
+        border-color: #409eff;
+      }
+    }
+    .publish-btn {
+      background: #409eff;
+      border-color: #409eff;
+    }
+  }
+
+  // 位置选择对话框
+  :deep(.el-dialog) {
+    border-radius: 12px;
+    .location-list {
+      max-height: 400px;
+      margin-top: 16px;
+      overflow-y: auto;
+      .location-item {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+        padding: 12px 16px;
+        cursor: pointer;
+        border-radius: 8px;
+        transition: all 0.3s;
+        &:hover {
+          background: #f5f7fa;
+        }
+        .el-icon {
+          font-size: 18px;
+          color: #909399;
+        }
+        span {
+          font-size: 15px;
+          color: #303133;
+        }
+      }
+    }
+  }
+}
+
+// 响应式适配
+@media (width <= 768px) {
+  .publish-dynamic-container {
+    .form-container {
+      padding: 20px 16px 100px;
+    }
+    .footer-actions {
+      padding: 12px 16px;
+      .el-button {
+        min-width: 140px;
+        height: 44px;
+        font-size: 15px;
+      }
+    }
+  }
+}
+</style>