Procházet zdrojové kódy

Merge branch 'development' of http://8.152.195.41:3000/alien/group_web_merchant into development

sgc před 2 měsíci
rodič
revize
14a450b6f4

+ 26 - 0
src/api/modules/storeDecoration.ts

@@ -2,6 +2,7 @@ import { ResPage, StoreUser } from "@/api/interface/index";
 import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 import httpApi from "@/api/indexApi";
+import { Upload } from "@/api/interface/index";
 
 /**
  * @name 商铺用户模块
@@ -30,6 +31,31 @@ export const getStoreDetail = params => {
   return httpApi.get(`/alienStorePlatform/storePlatformRenovation/getDecorationDetail`, params);
 };
 
+// 获取门店装修需求列表(分页)
+export const getDecorationPage = (params: any) => {
+  return httpApi.get(`/alienStore/renovation/requirement/getPage`, params);
+};
+
+// 获取装修需求详情
+export const getDecorationDetail = (params: { id: number | string }) => {
+  return httpApi.get(`/alienStore/renovation/requirement/getDetail`, params);
+};
+
+// 保存或更新装修需求
+export const saveOrUpdateDecoration = (params: any) => {
+  return httpApi.post(`/alienStore/renovation/requirement/saveOrUpdate`, params);
+};
+
+// 上传房屋图纸 - 使用 /alienStore/file/uploadMore 接口
+export const uploadDecorationImage = (params: FormData) => {
+  return httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false });
+};
+
+// 删除装修需求
+export const deleteDecoration = (params: { id: number | string }) => {
+  return httpApi.post(`/alienStore/renovation/requirement/delete`, {}, { params });
+};
+
 //保存店铺信息
 export const saveStoreInfo = (params: any) => {
   return httpApi.post(`/alienStore/store/info/saveOrUpdate`, params);

+ 665 - 0
src/views/storeDecoration/add.vue

@@ -0,0 +1,665 @@
+<template>
+  <div class="add-container">
+    <el-dialog v-model="dialogVisible" title="新建" width="800px" :close-on-click-modal="false" @close="handleClose">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="140px" label-position="right">
+        <!-- 需求标题 -->
+        <el-form-item label="需求标题" prop="requirementTitle" required>
+          <el-input v-model="formData.requirementTitle" placeholder="请输入" maxlength="100" clearable />
+        </el-form-item>
+
+        <!-- 装修类型 -->
+        <el-form-item label="装修类型" prop="renovationType">
+          <el-radio-group v-model="formData.renovationType">
+            <el-radio :label="1">新房装修</el-radio>
+            <el-radio :label="2">旧房改造</el-radio>
+            <el-radio :label="3">局部装修</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <!-- 房屋面积 -->
+        <el-form-item label="房屋面积(㎡)" prop="houseArea" required>
+          <el-input-number
+            v-model="formData.houseArea"
+            :min="0"
+            :max="99999"
+            :precision="2"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <!-- 装修预算 -->
+        <el-form-item label="装修预算(万元)" prop="renovationBudget" required>
+          <el-input-number
+            v-model="formData.renovationBudget"
+            :min="0"
+            :max="99999"
+            :precision="2"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <!-- 详细需求 -->
+        <el-form-item label="详细需求" prop="detailedRequirement">
+          <el-input
+            v-model="formData.detailedRequirement"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            maxlength="500"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+
+        <!-- 期望装修时间 -->
+        <el-form-item label="期望装修时间" prop="expectedRenovationTime" required>
+          <el-date-picker
+            v-model="formData.expectedRenovationTime"
+            type="date"
+            placeholder="请选择"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <!-- 上传房屋图纸 -->
+        <el-form-item label="上传房屋图纸" prop="attachmentUrls" required>
+          <el-upload
+            v-model:file-list="fileList"
+            action="#"
+            list-type="picture-card"
+            :limit="9"
+            :on-preview="handlePictureCardPreview"
+            :on-remove="handleRemove"
+            :on-exceed="handleExceed"
+            :before-upload="beforeUpload"
+            :http-request="handleImageUpload"
+            accept="image/*"
+            multiple
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+          <div class="upload-tip">({{ fileList.length }}/9)</div>
+        </el-form-item>
+
+        <!-- 联系人 -->
+        <el-form-item label="联系人" prop="contactName" required>
+          <el-input v-model="formData.contactName" placeholder="请输入" maxlength="50" clearable />
+        </el-form-item>
+
+        <!-- 联系电话 -->
+        <el-form-item label="联系电话" prop="contactPhone" required>
+          <el-input v-model="formData.contactPhone" placeholder="请输入" maxlength="20" clearable />
+        </el-form-item>
+
+        <!-- 所在城市 -->
+        <el-form-item label="所在城市" prop="city" required>
+          <el-input
+            v-model="formData.city"
+            placeholder="请选择"
+            readonly
+            @click="showCityDialog = true"
+            clearable
+            @clear="handleCityClear"
+          >
+            <template #suffix>
+              <el-icon class="cursor-pointer" @click="showCityDialog = true"><ArrowDown /></el-icon>
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <!-- 详细地址 -->
+        <el-form-item label="详细地址" prop="detailedAddress">
+          <el-input
+            v-model="formData.detailedAddress"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入"
+            maxlength="200"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+
+        <!-- 服务协议确认 -->
+        <el-form-item prop="agreementConfirmed">
+          <el-checkbox v-model="formData.agreementConfirmed" :true-label="1" :false-label="0">
+            我已阅读并同意
+            <el-link type="primary" :underline="false" @click="handleShowAgreement">《用户服务协议》</el-link>
+            和
+            <el-link type="primary" :underline="false" @click="handleShowPrivacy">《隐私政策》</el-link>
+            ,允许装修商家查看我的需求信息并与我联系
+          </el-checkbox>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleClose">返回</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 城市选择对话框 -->
+    <el-dialog v-model="showCityDialog" title="选择城市" width="600px">
+      <div class="city-selector">
+        <el-select
+          v-model="selectedProvince"
+          placeholder="请选择省"
+          clearable
+          style="width: 100%; margin-bottom: 10px"
+          @change="handleProvinceChange"
+        >
+          <el-option v-for="province in provinceOptions" :key="province.adcode" :label="province.name" :value="province.adcode" />
+        </el-select>
+        <el-select
+          v-model="selectedCity"
+          placeholder="请选择市"
+          clearable
+          style="width: 100%; margin-bottom: 10px"
+          :disabled="!selectedProvince"
+          @change="handleCitySelect"
+        >
+          <el-option v-for="city in cityOptions" :key="city.adcode" :label="city.name" :value="city.adcode" />
+        </el-select>
+        <el-select
+          v-model="selectedDistrict"
+          placeholder="请选择区"
+          clearable
+          style="width: 100%"
+          :disabled="!selectedCity"
+          @change="handleDistrictSelect"
+        >
+          <el-option v-for="district in districtOptions" :key="district.adcode" :label="district.name" :value="district.adcode" />
+        </el-select>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="showCityDialog = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmCity">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
+  </div>
+</template>
+
+<script setup lang="ts" name="decorationAdd">
+import { ref, reactive, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage, type FormInstance, type UploadFile, type UploadProps, type UploadRequestOptions } from "element-plus";
+import { Plus, ArrowDown } from "@element-plus/icons-vue";
+import { saveOrUpdateDecoration, getDistrict, uploadDecorationImage } from "@/api/modules/storeDecoration";
+import { localGet } from "@/utils";
+
+const route = useRoute();
+const router = useRouter();
+
+const dialogVisible = ref(true);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+
+const formData = reactive({
+  requirementTitle: "",
+  renovationType: 1,
+  houseArea: null as number | null,
+  renovationBudget: null as number | null,
+  detailedRequirement: "",
+  expectedRenovationTime: "",
+  attachmentUrls: [] as string[],
+  contactName: "",
+  contactPhone: "",
+  city: "",
+  cityAdcode: "",
+  detailedAddress: "",
+  agreementConfirmed: 0,
+  storeId: localGet("createdId") || 0
+});
+
+const fileList = ref<UploadFile[]>([]);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 城市选择相关
+const showCityDialog = ref(false);
+const selectedProvince = ref("");
+const selectedCity = ref("");
+const selectedDistrict = ref("");
+const provinceOptions = ref<any[]>([]);
+const cityOptions = ref<any[]>([]);
+const districtOptions = ref<any[]>([]);
+const selectedProvinceName = ref("");
+const selectedCityName = ref("");
+const selectedDistrictName = ref("");
+
+// 表单验证规则
+const rules = reactive({
+  requirementTitle: [{ required: true, message: "请输入需求标题", trigger: "blur" }],
+  houseArea: [{ required: true, message: "请输入房屋面积", trigger: "blur" }],
+  renovationBudget: [{ required: true, message: "请输入装修预算", trigger: "blur" }],
+  expectedRenovationTime: [{ required: true, message: "请选择期望装修时间", trigger: "change" }],
+  attachmentUrls: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        // 检查所有文件是否都已上传成功
+        const successFiles = fileList.value.filter((file: UploadFile) => file.status === "success");
+        if (successFiles.length === 0) {
+          callback(new Error("请上传房屋图纸"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ],
+  contactName: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  contactPhone: [
+    { required: true, message: "请输入联系电话", trigger: "blur" },
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
+  ],
+  city: [{ required: true, message: "请选择所在城市", trigger: "change" }],
+  agreementConfirmed: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (formData.agreementConfirmed !== 1) {
+          callback(new Error("请阅读并同意用户服务协议和隐私政策"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ]
+});
+
+// 获取省份数据
+const getProvinceData = async () => {
+  try {
+    const res: any = await getDistrict();
+    if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
+      const chinaData = res.data.districts[0];
+      if (chinaData && chinaData.districts && Array.isArray(chinaData.districts)) {
+        provinceOptions.value = chinaData.districts;
+      }
+    }
+  } catch (error) {
+    console.error("获取省份数据失败:", error);
+  }
+};
+
+// 省份变化时获取城市数据
+const handleProvinceChange = async (provinceCode: string) => {
+  selectedCity.value = "";
+  selectedDistrict.value = "";
+  cityOptions.value = [];
+  districtOptions.value = [];
+  selectedCityName.value = "";
+  selectedDistrictName.value = "";
+
+  if (!provinceCode) {
+    selectedProvinceName.value = "";
+    return;
+  }
+
+  const province = provinceOptions.value.find(p => p.adcode === provinceCode);
+  selectedProvinceName.value = province ? province.name : "";
+
+  try {
+    const res: any = await getDistrict({ adCode: provinceCode });
+    if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
+      const provinceData = res.data.districts[0];
+      if (provinceData && provinceData.districts && Array.isArray(provinceData.districts)) {
+        cityOptions.value = provinceData.districts;
+      }
+    }
+  } catch (error) {
+    console.error("获取城市数据失败:", error);
+  }
+};
+
+// 城市变化时获取区县数据
+const handleCitySelect = async (cityCode: string) => {
+  selectedDistrict.value = "";
+  districtOptions.value = [];
+  selectedDistrictName.value = "";
+
+  if (!cityCode) {
+    selectedCityName.value = "";
+    return;
+  }
+
+  const city = cityOptions.value.find(c => c.adcode === cityCode);
+  selectedCityName.value = city ? city.name : "";
+
+  try {
+    const res: any = await getDistrict({ adCode: cityCode });
+    if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
+      const cityData = res.data.districts[0];
+      if (cityData && cityData.districts && Array.isArray(cityData.districts)) {
+        districtOptions.value = cityData.districts;
+      }
+    }
+  } catch (error) {
+    console.error("获取区县数据失败:", error);
+  }
+};
+
+// 区县选择
+const handleDistrictSelect = (districtCode: string) => {
+  if (!districtCode) {
+    selectedDistrictName.value = "";
+    return;
+  }
+  const district = districtOptions.value.find(d => d.adcode === districtCode);
+  selectedDistrictName.value = district ? district.name : "";
+};
+
+// 确认城市选择
+const handleConfirmCity = () => {
+  if (!selectedProvince.value || !selectedCity.value) {
+    ElMessage.warning("请至少选择省和市");
+    return;
+  }
+  // 构建城市名称:省+市+区(如果有区)
+  let cityName = selectedProvinceName.value + selectedCityName.value;
+  if (selectedDistrictName.value) {
+    cityName += selectedDistrictName.value;
+  }
+  formData.city = cityName;
+  formData.cityAdcode = selectedDistrict.value || selectedCity.value || selectedProvince.value;
+  showCityDialog.value = false;
+};
+
+// 清除城市选择
+const handleCityClear = () => {
+  formData.city = "";
+  formData.cityAdcode = "";
+  selectedProvince.value = "";
+  selectedCity.value = "";
+  selectedDistrict.value = "";
+  selectedProvinceName.value = "";
+  selectedCityName.value = "";
+  selectedDistrictName.value = "";
+  cityOptions.value = [];
+  districtOptions.value = [];
+};
+
+// 图片上传前验证
+const beforeUpload: UploadProps["beforeUpload"] = (rawFile: File) => {
+  const isImage = rawFile.type.startsWith("image/");
+  const isLt10M = rawFile.size / 1024 / 1024 < 10;
+
+  if (!isImage) {
+    ElMessage.error("只能上传图片文件!");
+    return false;
+  }
+  if (!isLt10M) {
+    ElMessage.error("图片大小不能超过 10MB!");
+    return false;
+  }
+  return true;
+};
+
+// 图片上传 - 点击加号时调用 /file/uploadMore 接口
+const handleImageUpload = async (options: UploadRequestOptions) => {
+  // 获取文件对象,可能是 options.file 或 options.file.raw
+  const file = options.file.raw || options.file;
+  
+  if (!file) {
+    console.error("文件对象不存在");
+    ElMessage.error("文件对象不存在");
+    return;
+  }
+
+  console.log("开始上传文件:", file.name, "类型:", file.type, "大小:", file.size);
+
+  const uploadFormData = new FormData();
+  uploadFormData.append("file", file);
+
+  options.file.status = "uploading";
+  options.file.percentage = 0;
+
+  try {
+    console.log("调用 /alienStore/file/uploadMore 接口上传文件");
+    // 调用 /alienStore/file/uploadMore 接口上传文件
+    const result: any = await uploadDecorationImage(uploadFormData);
+    console.log("上传接口返回结果:", result);
+
+    if (result?.code === 200 || result?.code === 0) {
+      let fileUrl = "";
+
+      // 处理不同的返回格式
+      if (Array.isArray(result.data) && result.data.length > 0) {
+        fileUrl = result.data[0];
+      } else if (typeof result.data === "string") {
+        fileUrl = result.data;
+      } else if (result.data?.fileUrl) {
+        fileUrl = result.data.fileUrl;
+      } else if (result.data?.url) {
+        fileUrl = result.data.url;
+      } else if (result.fileUrl) {
+        fileUrl = result.fileUrl;
+      } else if (result.url) {
+        fileUrl = result.url;
+      }
+
+      if (fileUrl) {
+        options.file.status = "success";
+        options.file.percentage = 100;
+        options.file.url = fileUrl;
+        options.file.response = { url: fileUrl };
+
+        // 获取图片路径后,记录到 attachmentUrls 数组中,提交时会传递给保存接口
+        if (!formData.attachmentUrls.includes(fileUrl)) {
+          formData.attachmentUrls.push(fileUrl);
+          console.log("文件上传成功,路径已记录:", fileUrl);
+          console.log("当前附件列表:", formData.attachmentUrls);
+        }
+        options.onSuccess?.(result);
+        // 触发表单验证
+        formRef.value?.validateField("attachmentUrls");
+      } else {
+        console.error("上传接口返回数据格式错误:", result);
+        throw new Error("上传接口返回数据格式错误");
+      }
+    } else {
+      throw new Error(result?.msg || result?.message || "文件上传失败");
+    }
+  } catch (error: any) {
+    console.error("文件上传失败:", error);
+    options.file.status = "fail";
+    if (options.file.url && options.file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(options.file.url);
+    }
+    const index = fileList.value.findIndex(f => f.uid === options.file.uid);
+    if (index > -1) {
+      fileList.value.splice(index, 1);
+    }
+    ElMessage.error(error?.message || "文件上传失败");
+    options.onError?.(error);
+  }
+};
+
+// 图片预览
+const handlePictureCardPreview = (file: UploadFile) => {
+  if (file.url) {
+    imageViewerUrlList.value = fileList.value.map((item: UploadFile) => item.url || "").filter(Boolean);
+    imageViewerInitialIndex.value = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid);
+    imageViewerVisible.value = true;
+  }
+};
+
+// 删除图片
+const handleRemove = (file: UploadFile) => {
+  if (file.url) {
+    const index = formData.attachmentUrls.indexOf(file.url);
+    if (index > -1) {
+      formData.attachmentUrls.splice(index, 1);
+      console.log("删除图片,路径已移除:", file.url);
+      console.log("当前附件列表:", formData.attachmentUrls);
+    }
+  }
+  // 触发表单验证
+  formRef.value?.validateField("attachmentUrls");
+};
+
+// 超出限制
+const handleExceed = () => {
+  ElMessage.warning("最多只能上传9张图片");
+};
+
+// 显示服务协议
+const handleShowAgreement = () => {
+  // TODO: 打开服务协议页面
+  ElMessage.info("服务协议");
+};
+
+// 显示隐私政策
+const handleShowPrivacy = () => {
+  // TODO: 打开隐私政策页面
+  ElMessage.info("隐私政策");
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  await formRef.value.validate(async valid => {
+    if (!valid) return;
+
+    submitLoading.value = true;
+    try {
+      // 确保 attachmentUrls 是数组格式,包含所有上传成功的图片路径
+      const attachmentUrlsList = Array.isArray(formData.attachmentUrls) 
+        ? formData.attachmentUrls.filter(url => url && url.trim() !== "")
+        : [];
+
+      console.log("提交时的附件列表:", attachmentUrlsList);
+
+      // 构建提交参数 - attachmentUrls 字段存储图片路径数组
+      const params: any = {
+        id: 0, // 新建时传0
+        requirementTitle: formData.requirementTitle,
+        renovationType: formData.renovationType,
+        houseArea: formData.houseArea || 0,
+        renovationBudget: formData.renovationBudget || 0,
+        detailedRequirement: formData.detailedRequirement || "",
+        expectedRenovationTime: formData.expectedRenovationTime,
+        // 将上传获取的图片路径数组存入 attachmentUrls 字段,传递给保存接口
+        attachmentUrls: attachmentUrlsList,
+        contactName: formData.contactName,
+        contactPhone: formData.contactPhone,
+        city: formData.city,
+        cityAdcode: formData.cityAdcode || "",
+        detailedAddress: formData.detailedAddress || "",
+        agreementConfirmed: formData.agreementConfirmed,
+        storeId: formData.storeId || 0,
+        auditStatus: 0,
+        status: 0,
+        hasCommunicated: false,
+        inquiryCount: 0,
+        viewCount: 0,
+        createdTime: "",
+        updatedTime: "",
+        storeAddress: "",
+        storeAvatar: "",
+        storeBlurb: "",
+        storeName: "",
+        storeTel: ""
+      };
+
+      console.log("提交参数:", params);
+      const res: any = await saveOrUpdateDecoration(params);
+
+      if (res.code == 200 || res.code == 0) {
+        ElMessage.success("创建成功");
+        // 返回到列表页面
+        router.push("/storeDecorationManagement/decorationManagement");
+      } else {
+        ElMessage.error(res.msg || "创建失败");
+        // 创建失败也返回到列表页面
+        router.push("/storeDecorationManagement/decorationManagement");
+      }
+    } catch (error: any) {
+      ElMessage.error(error?.msg || error?.message || "创建失败");
+      // 创建失败也返回到列表页面
+      router.push("/storeDecorationManagement/decorationManagement");
+    } finally {
+      submitLoading.value = false;
+    }
+  });
+};
+
+// 关闭对话框
+const handleClose = () => {
+  dialogVisible.value = false;
+  // 返回到列表页面
+  router.push("/storeDecorationManagement/decorationManagement");
+};
+
+// 初始化
+onMounted(() => {
+  getProvinceData();
+});
+</script>
+
+<style lang="scss" scoped>
+.add-container {
+  :deep(.el-dialog__body) {
+    padding: 20px;
+    max-height: 70vh;
+    overflow-y: auto;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 20px;
+  }
+
+  :deep(.el-radio-group) {
+    display: flex;
+    gap: 20px;
+  }
+
+  .upload-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+
+  .dialog-footer {
+    text-align: center;
+    padding: 20px 0 0;
+  }
+
+  :deep(.el-upload--picture-card) {
+    width: 100px;
+    height: 100px;
+  }
+
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 100px;
+    height: 100px;
+  }
+
+  .city-selector {
+    padding: 10px 0;
+  }
+
+  .cursor-pointer {
+    cursor: pointer;
+  }
+}
+</style>

+ 288 - 0
src/views/storeDecoration/decorationManagement.vue

@@ -0,0 +1,288 @@
+<template>
+  <div class="table-box button-table">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <div class="action-buttons">
+          <el-button :icon="Plus" class="button" type="primary" @click="handleAdd"> 新增 </el-button>
+        </div>
+      </template>
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button v-if="scope.row.auditStatus !== '0'" link type="primary" @click="handleDelete(scope.row)">删除</el-button>
+        <el-button link type="primary" @click="toDetail(scope.row)">查看详情</el-button>
+      </template>
+    </ProTable>
+  </div>
+</template>
+
+<script setup lang="tsx" name="decorationManagement">
+import { reactive, ref, onMounted, onActivated } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import { getDecorationPage, deleteDecoration } from "@/api/modules/storeDecoration";
+import { localGet } from "@/utils";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+
+// 装修类型枚举
+const decorationTypeEnum = [
+  { label: "新房装修", value: "1" },
+  { label: "旧房改造", value: "2" },
+  { label: "局部装修", value: "3" }
+];
+
+// 审核状态枚举 (auditStatus: 0:待审核, 1:审核通过, 2:审核失败)
+const auditStatusEnum = [
+  { label: "待审核", value: "0" },
+  { label: "审核通过", value: "1" },
+  { label: "审核失败", value: "2" }
+];
+
+// 状态枚举 (status: 0:草稿, 1:已发布, 2:已下架)
+const statusEnum = [
+  { label: "草稿", value: "0" },
+  { label: "已发布", value: "1" },
+  { label: "已下架", value: "2" }
+];
+
+// 初始化请求参数
+const initParam = reactive({
+  storeId: localGet("createdId")
+});
+
+// 数据回调处理 - 根据图四的出参格式处理
+const dataCallback = (data: any) => {
+  // 出参格式: { code, data: { current, pages, records[], searchCount, size, total }, msg, success }
+  // httpApi 的响应拦截器会处理响应,返回的 data 可能是整个响应对象或 data 字段
+  // 如果 data 有 data 属性,说明是完整响应,需要取 data.data
+  // 如果 data 直接有 records,说明已经是处理后的数据
+  if (data && data.data && data.data.records) {
+    // 完整响应格式: { code, data: { records, total, ... }, msg, success }
+    return {
+      list: data.data.records || [],
+      total: data.data.total || 0
+    };
+  } else if (data && data.records) {
+    // 已经是 data 字段: { records, total, ... }
+    return {
+      list: data.records || [],
+      total: data.total || 0
+    };
+  }
+  return {
+    list: [],
+    total: 0
+  };
+};
+
+// 获取表格列表
+const getTableList = (params: any) => {
+  let newParams = JSON.parse(JSON.stringify(params));
+  
+  // 转换 ProTable 的分页参数为后端需要的参数
+  // ProTable 使用 pageNum 和 pageSize,后端使用 page 和 size
+  if (newParams.pageNum) {
+    newParams.page = newParams.pageNum;
+    delete newParams.pageNum;
+  }
+  if (newParams.pageSize) {
+    newParams.size = newParams.pageSize;
+    delete newParams.pageSize;
+  }
+  
+  // 处理日期范围 - createdTime 是日期范围选择器返回的数组
+  // 将日期范围转换为 startTime 和 endTime 参数(格式: yyyy-MM-dd)
+  if (newParams.createdTime && Array.isArray(newParams.createdTime) && newParams.createdTime.length === 2) {
+    newParams.startTime = newParams.createdTime[0]; // 开始时间
+    newParams.endTime = newParams.createdTime[1]; // 结束时间
+    delete newParams.createdTime;
+  }
+  
+  // 清理空值参数
+  Object.keys(newParams).forEach(key => {
+    if (newParams[key] === "" || newParams[key] === null || newParams[key] === undefined) {
+      delete newParams[key];
+    }
+  });
+  
+  // 确保 storeId 存在
+  if (!newParams.storeId) {
+    newParams.storeId = localGet("createdId");
+  }
+  
+  return getDecorationPage(newParams);
+};
+
+// 新增按钮点击事件
+const handleAdd = () => {
+  // TODO: 跳转到新增页面或打开新增对话框
+  router.push(`/storeDecorationManagement/add`);
+};
+
+// 跳转详情页
+const toDetail = (row: any) => {
+  router.push(`/storeDecorationManagement/detail?id=${row.id}`);
+};
+
+// 删除装修记录
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm("确定要删除这条装修记录吗?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning"
+  })
+    .then(async () => {
+      try {
+        // 调用删除接口 /renovation/requirement/delete
+        const res: any = await deleteDecoration({ id: row.id });
+        if (res.code === 200 || res.code === 0) {
+          ElMessage.success("删除成功");
+          // 刷新列表
+          proTable.value?.getTableList();
+        } else {
+          ElMessage.error(res.msg || "删除失败");
+        }
+      } catch (error: any) {
+        ElMessage.error(error?.msg || error?.message || "删除失败");
+      }
+    })
+    .catch(() => {
+      // 用户取消删除
+    });
+};
+
+// 表格列配置
+const columns = reactive<ColumnProps<any>[]>([
+  { type: "index", fixed: "left", label: "序号", width: 80 },
+  {
+    prop: "requirementTitle",
+    label: "标题",
+    render: (scope: any) => {
+      return scope.row.requirementTitle || "--";
+    },
+    search: {
+      el: "input",
+      props: { placeholder: "请输入" }
+    }
+  },
+  {
+    prop: "renovationType",
+    label: "装修类型",
+    render: (scope: any) => {
+      const type = decorationTypeEnum.find(item => item.value === String(scope.row.renovationType));
+      return type ? type.label : "--";
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: decorationTypeEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "houseArea",
+    label: "面积(㎡)",
+    render: (scope: any) => {
+      return scope.row.houseArea || "--";
+    }
+  },
+  {
+    prop: "renovationBudget",
+    label: "装修预算(万元)",
+    render: (scope: any) => {
+      return scope.row.renovationBudget || "--";
+    }
+  },
+  {
+    prop: "expectedRenovationTime",
+    label: "期望装修时间",
+    render: (scope: any) => {
+      if (scope.row.expectedRenovationTime) {
+        return scope.row.expectedRenovationTime.replace(/-/g, "/");
+      }
+      return "--";
+    }
+  },
+  {
+    prop: "auditStatus",
+    label: "状态",
+    render: (scope: any) => {
+      // 使用 auditStatus 字段判断状态
+      const auditStatus = scope.row.auditStatus;
+      // 如果 auditStatus 为空、null、undefined,显示 --
+      if (auditStatus === null || auditStatus === undefined || auditStatus === "") {
+        return "--";
+      }
+      const statusMap: Record<string, { text: string; type: string }> = {
+        "0": { text: "待审核", type: "warning" },
+        "1": { text: "审核通过", type: "success" },
+        "2": { text: "审核失败", type: "danger" }
+      };
+      const statusInfo = statusMap[String(auditStatus)];
+      if (!statusInfo) {
+        return "--";
+      }
+      return (
+        <el-tag type={statusInfo.type as any} size="small">
+          {statusInfo.text}
+        </el-tag>
+      );
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: auditStatusEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "createdTime",
+    label: "提交时间",
+    render: (scope: any) => {
+      if (scope.row.createdTime) {
+        return scope.row.createdTime.replace(/-/g, "/");
+      }
+      return "--";
+    },
+    search: {
+      el: "date-picker",
+      props: {
+        type: "daterange",
+        "range-separator": "至",
+        "start-placeholder": "请输入",
+        "end-placeholder": "请输入",
+        "value-format": "YYYY-MM-DD"
+      },
+      span: 2
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 180 }
+]);
+
+// 页面加载时触发查询
+onMounted(() => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+</script>
+
+<style lang="scss" scoped>
+.action-buttons {
+  display: flex;
+  flex: 0 0 auto;
+  gap: 10px;
+  margin-right: 20px;
+  .button {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 302 - 63
src/views/storeDecoration/detail.vue

@@ -1,93 +1,332 @@
 <template>
-  <div class="card content-box">
-    <el-form :model="formData" label-width="140px">
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="店铺名称 :">
-            <span>{{ formData.storeName }}</span>
-          </el-form-item>
-          <el-form-item label="名称 :">
-            <span>{{ formData.name }}</span>
-          </el-form-item>
-          <el-form-item label="描述 :">
-            <span>{{ formData.description }}</span>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="状态:">
-            <span>{{ getStatusName(formData.status) }}</span>
-          </el-form-item>
-          <el-form-item label="拒绝原因:" v-if="formData.status === '2'">
-            <span>{{ formData.rejectionReason }}</span>
-          </el-form-item>
-        </el-col>
-      </el-row>
-      <el-row class="text-center" style="margin-top: 20px">
-        <el-col :span="24">
-          <el-button type="primary" @click="goBack"> 确定 </el-button>
-        </el-col>
-      </el-row>
-    </el-form>
+  <div class="detail-container">
+    <div class="card content-box">
+      <div class="detail-header">
+        <h3>详情</h3>
+        <el-button text @click="handleClose">
+          <el-icon><Close /></el-icon>
+        </el-button>
+      </div>
+      <el-form ref="formRef" :model="formData" label-width="140px" label-position="right">
+        <el-row :gutter="40">
+          <!-- 左侧列 -->
+          <el-col :span="12">
+            <el-form-item label="需求标题" required>
+              <el-input v-model="formData.requirementTitle" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="装修类型">
+              <el-radio-group v-model="formData.renovationType" disabled>
+                <el-radio :label="1">新房装修</el-radio>
+                <el-radio :label="2">旧房改造</el-radio>
+                <el-radio :label="3">局部装修</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="房屋面积(㎡)" required>
+              <el-input v-model="formData.houseArea" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="装修预算(万元)" required>
+              <el-input v-model="formData.renovationBudget" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="详细需求">
+              <el-input
+                v-model="formData.detailedRequirement"
+                type="textarea"
+                :rows="4"
+                placeholder="请输入"
+                disabled
+              />
+            </el-form-item>
+
+            <el-form-item label="期望装修时间" required>
+              <el-date-picker
+                v-model="formData.expectedRenovationTime"
+                type="date"
+                placeholder="请选择"
+                value-format="YYYY-MM-DD"
+                style="width: 100%"
+                disabled
+              />
+            </el-form-item>
+
+            <el-form-item label="上传房屋图纸" required>
+              <el-upload
+                v-model:file-list="fileList"
+                list-type="picture-card"
+                :disabled="true"
+                :on-preview="handlePictureCardPreview"
+                :on-remove="handleRemove"
+              >
+                <el-icon><Plus /></el-icon>
+              </el-upload>
+              <div class="upload-tip">({{ fileList.length }}/9)</div>
+            </el-form-item>
+
+            <el-form-item label="联系人" required>
+              <el-input v-model="formData.contactName" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="联系电话" required>
+              <el-input v-model="formData.contactPhone" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="所在城市" required>
+              <el-input v-model="formData.city" placeholder="请选择" disabled />
+            </el-form-item>
+
+            <el-form-item label="详细地址">
+              <el-input
+                v-model="formData.detailedAddress"
+                type="textarea"
+                :rows="3"
+                placeholder="请输入"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+
+          <!-- 右侧列 -->
+          <el-col :span="12">
+            <el-form-item label="提交时间">
+              <el-input v-model="formData.createdTime" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="审核状态">
+              <el-input v-model="auditStatusText" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="审核时间">
+              <el-input v-model="formData.updatedTime" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item v-if="formData.auditStatus === 2" label="拒绝原因">
+              <el-input
+                v-model="formData.rejectionReason"
+                type="textarea"
+                :rows="4"
+                placeholder="请输入"
+                disabled
+              />
+              <div class="form-tip">根据审核状态 对应展示</div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div class="detail-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleConfirm">确定</el-button>
+      </div>
+    </div>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
   </div>
 </template>
 
-<script setup lang="tsx" name="storeDecorationDetail">
-import { ref, onMounted } from "vue";
+<script setup lang="ts" name="decorationDetail">
+import { ref, computed, onMounted, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getStaffConfigDeatail } from "@/api/modules/staffConfig";
+import { Plus, Close } from "@element-plus/icons-vue";
+import type { UploadFile } from "element-plus";
+import { getDecorationDetail } from "@/api/modules/storeDecoration";
 
 const route = useRoute();
 const router = useRouter();
 
-const formData = ref({});
+const formRef = ref();
+const formData = ref<any>({
+  requirementTitle: "",
+  renovationType: 1,
+  houseArea: "",
+  renovationBudget: "",
+  detailedRequirement: "",
+  expectedRenovationTime: "",
+  contactName: "",
+  contactPhone: "",
+  city: "",
+  detailedAddress: "",
+  createdTime: "",
+  auditStatus: 0,
+  updatedTime: "",
+  rejectionReason: "",
+  attachmentUrls: []
+});
 
-const id = ref((route.query.id as string) || "");
+const fileList = ref<UploadFile[]>([]);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
 
-const getStatusName = (status: string) => {
-  switch (status) {
-    case "0":
-      return "待审核";
-    case "1":
-      return "审核通过";
-    case "2":
-      return "审核拒绝";
-    default:
-      return "未知状态";
-  }
-};
-
-onMounted(async () => {
-  await initData();
+// 审核状态文本
+const auditStatusText = computed(() => {
+  const statusMap: Record<number, string> = {
+    0: "待审核",
+    1: "审核通过",
+    2: "审核失败"
+  };
+  return statusMap[formData.value.auditStatus] || "--";
 });
 
+// 获取详情数据
 const initData = async () => {
-  if (id.value) {
-    try {
-      const response = await getStaffConfigDeatail({ id: id.value });
-      if (response.code === 200) {
-        formData.value = response.data;
+  const id = route.query.id;
+  if (!id) {
+    handleClose();
+    return;
+  }
+
+  try {
+    const res = await getDecorationDetail({ id: id as string });
+    if (res.code == 200 || res.code == 0) {
+      const data = res.data || res.data?.data || {};
+      formData.value = {
+        requirementTitle: data.requirementTitle || "",
+        renovationType: data.renovationType || 1,
+        houseArea: data.houseArea || "",
+        renovationBudget: data.renovationBudget || "",
+        detailedRequirement: data.detailedRequirement || "",
+        expectedRenovationTime: data.expectedRenovationTime || "",
+        contactName: data.contactName || "",
+        contactPhone: data.contactPhone || "",
+        city: data.city || "",
+        detailedAddress: data.detailedAddress || "",
+        createdTime: data.createdTime || "",
+        auditStatus: data.auditStatus ?? 0,
+        updatedTime: data.updatedTime || "",
+        rejectionReason: data.rejectionReason || "",
+        attachmentUrls: data.attachmentUrls || []
+      };
+
+      // 处理附件列表
+      if (formData.value.attachmentUrls && formData.value.attachmentUrls.length > 0) {
+        fileList.value = formData.value.attachmentUrls.map((url: string, index: number) => ({
+          uid: index,
+          name: `图片${index + 1}`,
+          url: url,
+          status: "success"
+        }));
       }
-    } catch (error) {
-      ElMessage.error("获取详情失败");
     }
+  } catch (error: any) {
+    // 静默处理错误,不显示错误提示
+    console.error("获取详情失败:", error);
   }
 };
 
-const goBack = () => {
+// 图片预览
+const handlePictureCardPreview = (file: UploadFile) => {
+  if (file.url) {
+    imageViewerUrlList.value = fileList.value.map((item: UploadFile) => item.url || "").filter(Boolean);
+    imageViewerInitialIndex.value = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid);
+    imageViewerVisible.value = true;
+  }
+};
+
+// 删除图片(详情页禁用,这里只是占位)
+const handleRemove = () => {
+  // 详情页不允许删除
+};
+
+// 关闭页面
+const handleClose = () => {
   router.go(-1);
 };
+
+// 确定按钮
+const handleConfirm = () => {
+  handleClose();
+};
+
+// 监听路由变化
+watch(
+  () => route.query.id,
+  () => {
+    if (route.query.id) {
+      initData();
+    }
+  },
+  { immediate: true }
+);
+
+onMounted(() => {
+  if (route.query.id) {
+    initData();
+  }
+});
 </script>
 
 <style lang="scss" scoped>
-.el-form {
+.detail-container {
   width: 100%;
-  .text-center {
+  min-height: 100%;
+  background-color: white;
+
+  .content-box {
+    padding: 20px;
+  }
+
+  .detail-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #ebeef5;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 500;
+    }
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 20px;
+  }
+
+  :deep(.el-radio-group) {
+    display: flex;
+    gap: 20px;
+  }
+
+  .upload-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+
+  .form-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+    text-align: right;
+  }
+
+  .detail-footer {
     text-align: center;
+    padding: 20px 0 0;
+    margin-top: 20px;
+    border-top: 1px solid #ebeef5;
+  }
+
+  :deep(.el-upload--picture-card) {
+    width: 100px;
+    height: 100px;
+  }
+
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 100px;
+    height: 100px;
   }
-}
-.el-col {
-  box-sizing: border-box;
-  padding-right: 10px;
 }
 </style>