Procházet zdrojové kódy

feat(store): 完善菜单管理和官方相册功能

- 实现菜单管理页面的菜品展示、分页、新建、编辑、删除及推荐功能- 实现官方相册页面的相册创建、图片上传、预览、删除和封面设置功能
- 调整登录页面忘记密码和注册弹窗的高度以优化显示效果
- 登录成功后将用户信息存入 store 以便全局访问
spy před 1 měsícem
rodič
revize
c89d81d1ad

+ 3 - 2
src/views/login/index.vue

@@ -470,7 +470,7 @@
         </div>
       </template>
     </el-dialog>
-    <el-dialog title="忘记密码" v-model="forgetShow" style="height: 55vh">
+    <el-dialog title="忘记密码" v-model="forgetShow" style="height: 600px">
       <el-form ref="forgetFormRef" :model="forgetForm" :rules="forgetRules" size="large" class="login-form-content">
         <el-form-item prop="phone" label="手机号码" label-position="top">
           <el-input v-model="forgetForm.phone" placeholder="请输入手机号码" maxlength="11" clearable />
@@ -520,7 +520,7 @@
         </div>
       </template>
     </el-dialog>
-    <el-dialog title="注册账号" v-model="registerShow" style="height: 65vh">
+    <el-dialog title="注册账号" v-model="registerShow" style="height: 700px">
       <el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" size="large" class="login-form-content">
         <el-form-item prop="phone" label="手机号码" label-position="top">
           <el-input v-model="registerForm.phone" placeholder="请输入手机号码" maxlength="11" clearable />
@@ -875,6 +875,7 @@ const handleLogin = async () => {
       if (res.data) {
         console.log(res.data, "res.data");
         userStore.setToken(res.data.token);
+        userStore.setUserInfo(res.data);
         await initDynamicRouter();
 
         // 3.清空 tabs、keepAlive 数据

+ 559 - 4
src/views/storeDecoration/menuManagement/index.vue

@@ -1,7 +1,562 @@
-<script setup lang="ts"></script>
-
 <template>
-  <div>1</div>
+  <div class="menu-management-container">
+    <!-- 头部:Tabs和新建按钮 -->
+    <div class="header-section">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane label="菜单" name="menu" />
+        <el-tab-pane label="推荐菜" name="recommended" />
+      </el-tabs>
+      <el-button type="primary" @click="openCreateDialog"> 新建菜品 </el-button>
+    </div>
+
+    <!-- 内容区域 -->
+    <div v-if="displayDishList.length > 0" class="content-section">
+      <!-- 菜品卡片网格 -->
+      <div class="dish-grid">
+        <div v-for="(dish, index) in paginatedDishList" :key="dish.id" class="dish-card">
+          <div class="dish-image-wrapper">
+            <img v-if="dish.image" :src="dish.image" alt="菜品图片" />
+            <div v-else class="image-placeholder">
+              <el-icon><Picture /></el-icon>
+            </div>
+            <el-tag v-if="dish.isRecommended" type="primary" class="recommend-tag"> 推荐 </el-tag>
+          </div>
+          <div class="dish-info">
+            <div class="dish-name">
+              {{ dish.name }}
+            </div>
+            <div class="dish-price">¥{{ dish.price }}/{{ dish.unit }}</div>
+            <div v-if="activeTab === 'recommended'" class="dish-recommend-count">{{ dish.recommendCount || 0 }}人推荐</div>
+          </div>
+          <div class="dish-actions">
+            <el-button type="primary" link @click="editDish(dish, index)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deleteDish(dish.id, index)"> 删除 </el-button>
+            <el-button v-if="dish.isRecommended" type="primary" link @click="cancelRecommend(dish.id, index)">
+              取消推荐
+            </el-button>
+            <el-button v-else type="primary" link @click="setRecommend(dish.id, index)"> 设为推荐 </el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 分页 -->
+      <div class="pagination-section">
+        <el-pagination
+          v-model:current-page="pageable.pageNum"
+          v-model:page-size="pageable.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="pageable.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <div v-else class="empty-state">
+      <el-empty description="暂无数据" />
+      <el-button type="primary" @click="openCreateDialog"> 新建菜品 </el-button>
+    </div>
+
+    <!-- 新建/编辑菜品弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="resetForm">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+        <el-form-item label="菜品名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入" maxlength="10" show-word-limit clearable />
+        </el-form-item>
+        <el-form-item label="价格 (¥)" prop="price">
+          <el-input-number
+            v-model="formData.price"
+            :min="0"
+            :max="99999.99"
+            :precision="2"
+            :step="0.01"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="成本价 (¥)" prop="costPrice">
+          <el-input-number
+            v-model="formData.costPrice"
+            :min="0"
+            :max="99999.99"
+            :precision="2"
+            :step="0.01"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="单位" prop="unit">
+          <el-select v-model="formData.unit" placeholder="请选择" style="width: 100%">
+            <el-option v-for="unit in unitOptions" :key="unit.value" :label="unit.label" :value="unit.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="图片" prop="image">
+          <UploadImg
+            v-model:image-url="formData.image"
+            :width="'200px'"
+            :height="'200px'"
+            :file-size="10"
+            :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+            :border-radius="'8px'"
+          />
+        </el-form-item>
+        <el-form-item label="菜品描述" prop="description">
+          <el-input
+            v-model="formData.description"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            maxlength="300"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+        <el-form-item label="" prop="isRecommended">
+          <el-checkbox v-model="formData.isRecommended"> 设为推荐 </el-checkbox>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false"> 取消 </el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit"> 新建菜品 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
-<style scoped lang="scss"></style>
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormInstance, FormRules } from "element-plus";
+import { Picture } from "@element-plus/icons-vue";
+import UploadImg from "@/components/Upload/Img.vue";
+// TODO: 导入菜单相关的 API 接口
+// import { getDishList, createDish, updateDish, deleteDish, setRecommendDish, cancelRecommendDish } from "@/api/modules/storeDecoration";
+
+// 菜品接口
+interface Dish {
+  id: string;
+  name: string;
+  price: number;
+  costPrice: number;
+  unit: string;
+  image: string;
+  description?: string;
+  isRecommended: boolean;
+  recommendCount?: number;
+}
+
+const dialogVisible = ref(false);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+const activeTab = ref<"menu" | "recommended">("menu");
+const editIndex = ref<number | null>(null);
+
+// 单位选项
+const unitOptions = [
+  { label: "份", value: "份" },
+  { label: "个", value: "个" },
+  { label: "碗", value: "碗" },
+  { label: "杯", value: "杯" },
+  { label: "盘", value: "盘" },
+  { label: "次", value: "次" },
+  { label: "节", value: "节" }
+];
+
+// 菜品列表
+const dishList = ref<Dish[]>([]);
+
+// 显示菜品列表(根据tab过滤)
+const displayDishList = computed(() => {
+  if (activeTab.value === "recommended") {
+    return dishList.value.filter(dish => dish.isRecommended);
+  }
+  return dishList.value;
+});
+
+// 分页数据
+const pageable = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 分页后的菜品列表
+const paginatedDishList = computed(() => {
+  const list = displayDishList.value;
+  const start = (pageable.pageNum - 1) * pageable.pageSize;
+  const end = start + pageable.pageSize;
+  return list.slice(start, end);
+});
+
+// 弹窗标题
+const dialogTitle = computed(() => (editIndex.value !== null ? "编辑菜品" : "新建菜品"));
+
+// 表单数据
+const formData = reactive({
+  name: "",
+  price: undefined as number | undefined,
+  costPrice: undefined as number | undefined,
+  unit: "份",
+  image: "",
+  description: "",
+  isRecommended: true
+});
+
+// 表单校验规则
+const rules = reactive<FormRules>({
+  name: [
+    { required: true, message: "请输入菜品名称", trigger: "blur" },
+    { max: 10, message: "菜品名称不能超过10个字", trigger: "blur" }
+  ],
+  price: [
+    { required: true, message: "请输入价格", trigger: "blur" },
+    {
+      validator: (rule, value, callback) => {
+        if (value === undefined || value === null || value === "") {
+          callback(new Error("请输入价格"));
+          return;
+        }
+        if (value < 0 || value > 99999.99) {
+          callback(new Error("价格必须在0-99999.99之间"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  costPrice: [
+    { required: true, message: "请输入成本价", trigger: "blur" },
+    {
+      validator: (rule, value, callback) => {
+        if (value === undefined || value === null || value === "") {
+          callback(new Error("请输入成本价"));
+          return;
+        }
+        if (value < 0 || value > 99999.99) {
+          callback(new Error("成本价必须在0-99999.99之间"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  unit: [{ required: true, message: "请选择单位", trigger: "change" }],
+  image: [{ required: true, message: "请上传图片", trigger: "change" }],
+  description: [{ max: 300, message: "描述不能超过300个字", trigger: "blur" }]
+});
+
+// Tab切换
+const handleTabClick = () => {
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 更新分页总数
+const updatePagination = () => {
+  pageable.total = displayDishList.value.length;
+};
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pageable.pageSize = size;
+  pageable.pageNum = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pageable.pageNum = page;
+};
+
+// 打开新建弹窗
+const openCreateDialog = () => {
+  editIndex.value = null;
+  resetForm();
+  dialogVisible.value = true;
+};
+
+// 编辑菜品
+const editDish = (dish: Dish, index: number) => {
+  // 在完整列表中查找索引
+  const actualIndex = dishList.value.findIndex(d => d.id === dish.id);
+  editIndex.value = actualIndex > -1 ? actualIndex : null;
+  formData.name = dish.name;
+  formData.price = dish.price;
+  formData.costPrice = dish.costPrice;
+  formData.unit = dish.unit;
+  formData.image = dish.image;
+  formData.description = dish.description || "";
+  formData.isRecommended = dish.isRecommended;
+  dialogVisible.value = true;
+};
+
+// 删除菜品
+const deleteDish = async (id: string, index: number) => {
+  // TODO: 调用删除菜品接口 deleteDish API
+  // await deleteDish({ id });
+
+  const actualIndex = dishList.value.findIndex(d => d.id === id);
+  if (actualIndex > -1) {
+    dishList.value.splice(actualIndex, 1);
+    updatePagination();
+    // 如果当前页没有数据了,回到上一页
+    if (paginatedDishList.value.length === 0 && pageable.pageNum > 1) {
+      pageable.pageNum--;
+    }
+    ElMessage.success("删除成功");
+  }
+};
+
+// 设为推荐
+const setRecommend = async (id: string, index: number) => {
+  // TODO: 调用设为推荐接口 setRecommendDish API
+  // await setRecommendDish({ id });
+
+  const dish = dishList.value.find(d => d.id === id);
+  if (dish) {
+    dish.isRecommended = true;
+    dish.recommendCount = (dish.recommendCount || 0) + 1;
+    ElMessage.success("设置推荐成功");
+  }
+};
+
+// 取消推荐
+const cancelRecommend = async (id: string, index: number) => {
+  // TODO: 调用取消推荐接口 cancelRecommendDish API
+  // await cancelRecommendDish({ id });
+
+  const dish = dishList.value.find(d => d.id === id);
+  if (dish) {
+    dish.isRecommended = false;
+    ElMessage.success("取消推荐成功");
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  formData.name = "";
+  formData.price = undefined;
+  formData.costPrice = undefined;
+  formData.unit = "份";
+  formData.image = "";
+  formData.description = "";
+  formData.isRecommended = true;
+  formRef.value?.clearValidate();
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  try {
+    await formRef.value.validate();
+  } catch (error) {
+    ElMessage.warning("请完善必填项");
+    return;
+  }
+
+  submitLoading.value = true;
+  try {
+    if (editIndex.value !== null && editIndex.value < dishList.value.length) {
+      // 编辑菜品
+      // TODO: 调用更新菜品接口 updateDish API
+      // const params = {
+      //   id: dishList.value[editIndex.value].id,
+      //   ...formData
+      // };
+      // await updateDish(params);
+
+      const dish = dishList.value[editIndex.value];
+      const wasRecommended = dish.isRecommended;
+      dish.name = formData.name;
+      dish.price = formData.price!;
+      dish.costPrice = formData.costPrice!;
+      dish.unit = formData.unit;
+      dish.image = formData.image;
+      dish.description = formData.description;
+      dish.isRecommended = formData.isRecommended;
+
+      // 如果从非推荐变为推荐,增加推荐人数
+      if (formData.isRecommended && !wasRecommended) {
+        dish.recommendCount = (dish.recommendCount || 0) + 1;
+      }
+
+      ElMessage.success("编辑成功");
+    } else {
+      // 新建菜品
+      // TODO: 调用创建菜品接口 createDish API
+      // const params = {
+      //   ...formData,
+      //   recommendCount: formData.isRecommended ? 1 : 0
+      // };
+      // const res = await createDish(params);
+      // dishList.value.push(res.data);
+
+      const newDish: Dish = {
+        id: `dish_${Date.now()}`,
+        name: formData.name,
+        price: formData.price!,
+        costPrice: formData.costPrice!,
+        unit: formData.unit,
+        image: formData.image,
+        description: formData.description,
+        isRecommended: formData.isRecommended,
+        recommendCount: formData.isRecommended ? 1 : 0
+      };
+
+      dishList.value.push(newDish);
+      ElMessage.success("新建成功");
+    }
+
+    dialogVisible.value = false;
+    updatePagination();
+    resetForm();
+  } catch (error) {
+    ElMessage.error("操作失败,请重试");
+    console.error("操作失败:", error);
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+// 页面初始化
+onMounted(async () => {
+  // TODO: 调用获取菜品列表接口 getDishList API
+  // const res = await getDishList();
+  // if (res.data) {
+  //   dishList.value = res.data;
+  //   updatePagination();
+  // }
+
+  // 示例数据
+  // dishList.value = [
+  //   {
+  //     id: "1",
+  //     name: "火锅锅底",
+  //     price: 38,
+  //     costPrice: 20,
+  //     unit: "份",
+  //     image: "",
+  //     description: "",
+  //     isRecommended: true,
+  //     recommendCount: 1
+  //   }
+  // ];
+  updatePagination();
+});
+</script>
+
+<style scoped lang="scss">
+.menu-management-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 24px;
+    :deep(.el-tabs) {
+      flex: 1;
+      .el-tabs__header {
+        margin: 0;
+      }
+    }
+  }
+  .content-section {
+    .dish-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+      gap: 20px;
+      margin-bottom: 24px;
+      .dish-card {
+        overflow: hidden;
+        background-color: white;
+        border-radius: 8px;
+        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
+        transition: all 0.3s;
+        &:hover {
+          box-shadow: 0 4px 16px 0 rgb(0 0 0 / 15%);
+        }
+        .dish-image-wrapper {
+          position: relative;
+          width: 100%;
+          height: 180px;
+          overflow: hidden;
+          background-color: #f0f0f0;
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .image-placeholder {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 100%;
+            height: 100%;
+            color: #909399;
+            .el-icon {
+              font-size: 48px;
+            }
+          }
+          .recommend-tag {
+            position: absolute;
+            top: 8px;
+            right: 8px;
+          }
+        }
+        .dish-info {
+          padding: 12px 16px;
+          .dish-name {
+            margin-bottom: 8px;
+            font-size: 16px;
+            font-weight: bold;
+            color: #000000;
+          }
+          .dish-price {
+            margin-bottom: 4px;
+            font-size: 14px;
+            font-weight: bold;
+            color: #f56c6c;
+          }
+          .dish-recommend-count {
+            font-size: 12px;
+            color: #909399;
+          }
+        }
+        .dish-actions {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 8px;
+          padding: 12px 16px;
+          border-top: 1px solid #f0f0f0;
+        }
+      }
+    }
+    .pagination-section {
+      display: flex;
+      justify-content: flex-end;
+      margin-top: 24px;
+    }
+  }
+  .empty-state {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    align-items: center;
+    justify-content: center;
+    min-height: 400px;
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+  }
+}
+</style>

+ 466 - 4
src/views/storeDecoration/officialPhotoAlbum/index.vue

@@ -1,7 +1,469 @@
-<script setup lang="ts"></script>
-
 <template>
-  <div>1</div>
+  <div class="photo-album-container">
+    <!-- 头部:Tabs和新建按钮 -->
+    <div class="header-section">
+      <el-tabs v-if="albumList.length > 0" v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane v-for="album in albumList" :key="album.id" :label="album.name" :name="album.id" />
+      </el-tabs>
+      <el-button type="primary" @click="openCreateDialog"> 新建相册 </el-button>
+    </div>
+
+    <!-- 内容区域 -->
+    <div v-if="albumList.length > 0" class="content-section">
+      <!-- 图片上传说明 -->
+      <div class="upload-tips">
+        <span>照片宽高不小于500像素</span>
+        <span>大小不超过10M</span>
+        <span>暂不支持上传动图</span>
+        <span>不可上传手机截屏、含清晰人脸/电话/二维码/第三方水印/LOGO等图片。</span>
+      </div>
+
+      <!-- 图片上传和展示区域 -->
+      <div class="image-grid">
+        <!-- 上传按钮 -->
+        <div class="upload-item upload-box">
+          <UploadImgs
+            v-model:file-list="currentAlbum.images"
+            :limit="60"
+            :file-size="10"
+            :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+            :width="'200px'"
+            :height="'200px'"
+            :border-radius="'8px'"
+            :api="customUploadApi"
+          >
+            <template #tip>
+              <div class="upload-tip-text">上传图片 ({{ currentAlbum.images.length }}/60)</div>
+            </template>
+          </UploadImgs>
+        </div>
+
+        <!-- 已上传的图片 -->
+        <div v-for="(image, index) in displayImages" :key="image.uid || index" class="image-item">
+          <div class="image-wrapper">
+            <img :src="image.url" alt="相册图片" />
+            <div class="image-overlay">
+              <el-button type="primary" link @click="viewImage(image.url)"> 查看 </el-button>
+              <el-button type="primary" link @click="deleteImage(index)"> 删除 </el-button>
+            </div>
+            <el-tag v-if="image.isCover" type="danger" class="cover-tag"> 封面 </el-tag>
+            <el-button v-else type="primary" size="small" class="set-cover-btn" @click="setCover(index)"> 设为封面 </el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 分页 -->
+      <div class="pagination-section">
+        <el-pagination
+          v-model:current-page="pageable.pageNum"
+          v-model:page-size="pageable.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="pageable.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <div v-else class="empty-state">
+      <div class="empty-text">暂无相册,快来创建吧</div>
+    </div>
+
+    <!-- 新建相册弹窗 -->
+    <el-dialog v-model="createDialogVisible" title="新建相册" width="500px" @close="resetCreateForm">
+      <el-form :model="createForm" :rules="createRules" ref="createFormRef" label-width="0">
+        <el-form-item prop="name">
+          <el-input v-model="createForm.name" placeholder="请输入相册名称" maxlength="50" clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="createDialogVisible = false"> 取消 </el-button>
+          <el-button type="primary" @click="handleCreateAlbum"> 新建 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览对话框 -->
+    <el-dialog v-model="previewVisible" width="800px" align-center>
+      <img :src="previewImageUrl" alt="预览图片" style="width: 100%; height: auto" />
+    </el-dialog>
+  </div>
 </template>
 
-<style scoped lang="scss"></style>
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted, watch } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormInstance, FormRules } from "element-plus";
+import type { UploadUserFile } from "element-plus";
+import UploadImgs from "@/components/Upload/Imgs.vue";
+import { uploadImg } from "@/api/modules/upload";
+// TODO: 导入相册相关的 API 接口
+// import { getAlbumList, createAlbum, deleteAlbum, getAlbumImages, uploadAlbumImage, deleteAlbumImage, setCoverImage } from "@/api/modules/storeDecoration";
+
+// 相册接口
+interface Album {
+  id: string;
+  name: string;
+  images: UploadUserFile[];
+  coverImageId?: string;
+}
+
+const createDialogVisible = ref(false);
+const createFormRef = ref<FormInstance>();
+const activeTab = ref("");
+const previewVisible = ref(false);
+const previewImageUrl = ref("");
+
+// 相册列表
+const albumList = ref<Album[]>([]);
+
+// 当前相册
+const currentAlbum = computed(() => {
+  if (albumList.value.length === 0) {
+    return { id: "", name: "", images: [] };
+  }
+  return albumList.value.find(album => album.id === activeTab.value) || albumList.value[0];
+});
+
+// 分页数据
+const pageable = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 显示图片列表(分页后)
+const displayImages = computed(() => {
+  if (!currentAlbum.value || !currentAlbum.value.images) {
+    return [];
+  }
+  const images = currentAlbum.value.images;
+  const start = (pageable.pageNum - 1) * pageable.pageSize;
+  const end = start + pageable.pageSize;
+  return images.slice(start, end);
+});
+
+// 新建相册表单
+const createForm = reactive({
+  name: ""
+});
+
+// 新建相册表单校验规则
+const createRules = reactive<FormRules>({
+  name: [
+    { required: true, message: "请输入相册名称", trigger: "blur" },
+    { min: 1, max: 50, message: "相册名称长度在1到50个字符", trigger: "blur" }
+  ]
+});
+
+// 图片尺寸校验函数(不小于500像素)
+const validateImageDimensions = (file: File): Promise<boolean> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = (e: ProgressEvent<FileReader>) => {
+      const img = new Image();
+      img.onload = () => {
+        const width = img.width;
+        const height = img.height;
+
+        // 检查尺寸:不小于500像素
+        if (width < 500 || height < 500) {
+          ElMessage.warning("照片宽高不小于500像素");
+          reject(new Error("图片尺寸不符合要求"));
+          return;
+        }
+
+        resolve(true);
+      };
+      img.onerror = () => {
+        reject(new Error("图片加载失败"));
+      };
+      if (e.target?.result) {
+        img.src = e.target.result as string;
+      }
+    };
+    reader.onerror = () => {
+      reject(new Error("文件读取失败"));
+    };
+    reader.readAsDataURL(file);
+  });
+};
+
+// 自定义上传API
+const customUploadApi = async (formData: FormData): Promise<any> => {
+  const file = formData.get("file") as File;
+
+  // 先进行尺寸校验
+  try {
+    await validateImageDimensions(file);
+  } catch (error) {
+    throw error;
+  }
+
+  // 校验通过后调用实际上传接口
+  return uploadImg(formData);
+};
+
+// 打开新建相册弹窗
+const openCreateDialog = () => {
+  createDialogVisible.value = true;
+  resetCreateForm();
+};
+
+// 重置新建表单
+const resetCreateForm = () => {
+  createForm.name = "";
+  createFormRef.value?.clearValidate();
+};
+
+// 创建相册
+const handleCreateAlbum = async () => {
+  if (!createFormRef.value) return;
+
+  try {
+    await createFormRef.value.validate();
+  } catch (error) {
+    return;
+  }
+
+  // TODO: 调用创建相册接口 createAlbum API
+  // const res = await createAlbum({ name: createForm.name });
+  // const newAlbum: Album = {
+  //   id: res.data.id,
+  //   name: createForm.name,
+  //   images: []
+  // };
+
+  // 创建新相册
+  const newAlbum: Album = {
+    id: `album_${Date.now()}`,
+    name: createForm.name,
+    images: []
+  };
+
+  albumList.value.push(newAlbum);
+  activeTab.value = newAlbum.id;
+  createDialogVisible.value = false;
+  ElMessage.success("创建成功");
+  resetCreateForm();
+};
+
+// Tab切换
+const handleTabClick = (tab: any) => {
+  activeTab.value = tab.props.name;
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 查看图片
+const viewImage = (url: string) => {
+  previewImageUrl.value = url;
+  previewVisible.value = true;
+};
+
+// 删除图片
+const deleteImage = (index: number) => {
+  const actualIndex = (pageable.pageNum - 1) * pageable.pageSize + index;
+  const image = currentAlbum.value.images[actualIndex];
+
+  // TODO: 调用删除图片接口 deleteAlbumImage API
+  // await deleteAlbumImage({ albumId: currentAlbum.value.id, imageId: image.id });
+
+  currentAlbum.value.images.splice(actualIndex, 1);
+  updatePagination();
+  ElMessage.success("删除成功");
+};
+
+// 设置封面
+const setCover = (index: number) => {
+  const actualIndex = (pageable.pageNum - 1) * pageable.pageSize + index;
+  const image = currentAlbum.value.images[actualIndex];
+
+  // TODO: 调用设置封面接口 setCoverImage API
+  // await setCoverImage({ albumId: currentAlbum.value.id, imageId: image.id });
+
+  // 清除其他图片的封面标记
+  currentAlbum.value.images.forEach(img => {
+    img.isCover = false;
+  });
+  // 设置当前图片为封面
+  image.isCover = true;
+  currentAlbum.value.coverImageId = image.uid;
+
+  ElMessage.success("设置封面成功");
+};
+
+// 更新分页总数
+const updatePagination = () => {
+  if (currentAlbum.value && currentAlbum.value.images) {
+    pageable.total = currentAlbum.value.images.length;
+  } else {
+    pageable.total = 0;
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pageable.pageSize = size;
+  pageable.pageNum = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pageable.pageNum = page;
+};
+
+// 监听图片列表变化,更新分页
+watch(
+  () => currentAlbum.value?.images?.length || 0,
+  () => {
+    updatePagination();
+  }
+);
+
+// 监听相册切换,更新分页
+watch(
+  () => activeTab.value,
+  () => {
+    updatePagination();
+    pageable.pageNum = 1;
+  }
+);
+
+// 页面初始化
+onMounted(async () => {
+  // TODO: 调用获取相册列表接口 getAlbumList API
+  // const res = await getAlbumList();
+  // if (res.data && res.data.length > 0) {
+  //   albumList.value = res.data.map(album => ({
+  //     id: album.id,
+  //     name: album.name,
+  //     images: [],
+  //     coverImageId: album.coverImageId
+  //   }));
+  //   activeTab.value = albumList.value[0].id;
+  //   // 加载第一个相册的图片
+  //   await loadAlbumImages(albumList.value[0].id);
+  // }
+  // 初始化默认相册(示例数据)
+  // albumList.value = [
+  //   { id: "1", name: "环境氛围", images: [] },
+  //   { id: "2", name: "餐饮", images: [] }
+  // ];
+  // if (albumList.value.length > 0) {
+  //   activeTab.value = albumList.value[0].id;
+  // }
+});
+</script>
+
+<style scoped lang="scss">
+.photo-album-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 24px;
+    :deep(.el-tabs) {
+      flex: 1;
+      .el-tabs__header {
+        margin: 0;
+      }
+    }
+  }
+  .content-section {
+    .upload-tips {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 16px;
+      margin-bottom: 20px;
+      font-size: 14px;
+      color: #606266;
+    }
+    .image-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+      gap: 16px;
+      margin-bottom: 24px;
+      .upload-item {
+        min-height: 200px;
+      }
+      .image-item {
+        position: relative;
+        min-height: 200px;
+        .image-wrapper {
+          position: relative;
+          width: 100%;
+          height: 200px;
+          overflow: hidden;
+          background-color: #f0f0f0;
+          border-radius: 8px;
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .image-overlay {
+            position: absolute;
+            inset: 0;
+            display: flex;
+            gap: 12px;
+            align-items: center;
+            justify-content: center;
+            background-color: rgb(0 0 0 / 60%);
+            opacity: 0;
+            transition: opacity 0.3s;
+            .el-button {
+              color: white;
+            }
+          }
+          &:hover .image-overlay {
+            opacity: 1;
+          }
+          .cover-tag {
+            position: absolute;
+            top: 8px;
+            right: 8px;
+          }
+          .set-cover-btn {
+            position: absolute;
+            top: 8px;
+            right: 8px;
+          }
+        }
+      }
+    }
+    .pagination-section {
+      display: flex;
+      justify-content: center;
+      margin-top: 24px;
+    }
+  }
+  .empty-state {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-height: 400px;
+    .empty-text {
+      font-size: 16px;
+      color: #909399;
+    }
+  }
+  .upload-tip-text {
+    margin-top: 8px;
+    font-size: 12px;
+    color: #909399;
+    text-align: center;
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+  }
+}
+</style>