lxr 2 月之前
父節點
當前提交
c410fdabab

+ 45 - 2
src/api/modules/homeEntry.ts

@@ -1,15 +1,58 @@
+import axios from "axios";
 import { ResPage, StoreUser } from "@/api/interface/index";
 import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
+import { ResultEnum } from "@/enums/httpEnum";
+import { useUserStore } from "@/stores/modules/user";
+import { ElMessage } from "element-plus";
+import { LOGIN_URL } from "@/config";
+import router from "@/routers";
+
+const httpStore = axios.create({
+  baseURL: import.meta.env.VITE_API_URL_STORE as string,
+  timeout: ResultEnum.TIMEOUT as number,
+  withCredentials: true
+});
+httpStore.interceptors.request.use(
+  config => {
+    const userStore = useUserStore();
+    if (config.headers) {
+      (config.headers as any).Authorization = userStore.token;
+    }
+    return config;
+  },
+  error => Promise.reject(error)
+);
+httpStore.interceptors.response.use(
+  response => {
+    const data = response.data;
+    const userStore = useUserStore();
+    if (data.code == ResultEnum.OVERDUE) {
+      userStore.setToken("");
+      router.replace(LOGIN_URL);
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    if (data.code && data.code !== ResultEnum.SUCCESS) {
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    return data;
+  },
+  error => {
+    ElMessage.error("请求失败,请稍后重试");
+    return Promise.reject(error);
+  }
+);
 
 //个人实名
 export const verifyIdInfo = params => {
   return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/merchantAuth/verifyIdInfo`, params);
 };
 
-//信息提交
+// 信息提交(店铺入驻,使用 alienStore 前缀)
 export const applyStore = params => {
-  return http.post(PORT_NONE + `/storeManage/applyStore`, params);
+  return httpStore.post(PORT_NONE + `/store/info/saveStoreInfo`, params);
 };
 
 //用户信息查询

+ 358 - 0
src/api/modules/performance.ts

@@ -0,0 +1,358 @@
+/**
+ * 演出模块 API(商家端 store 接口)
+ * 与 group_merchant barPerformance 接口保持一致
+ */
+import axios from "axios";
+import { PORT_NONE } from "@/api/config/servicePort";
+import { localGet } from "@/utils";
+import { ResultEnum } from "@/enums/httpEnum";
+import { useUserStore } from "@/stores/modules/user";
+import { ElMessage } from "element-plus";
+import { LOGIN_URL } from "@/config";
+import router from "@/routers";
+
+const performanceAxios = axios.create({
+  baseURL: import.meta.env.VITE_API_URL_STORE as string,
+  timeout: ResultEnum.TIMEOUT as number,
+  withCredentials: true
+});
+
+performanceAxios.interceptors.request.use(
+  config => {
+    const userStore = useUserStore();
+    if (config.headers) {
+      (config.headers as any).Authorization = userStore.token;
+    }
+    return config;
+  },
+  error => Promise.reject(error)
+);
+
+performanceAxios.interceptors.response.use(
+  response => {
+    const data = response.data;
+    const userStore = useUserStore();
+    if (data.code == ResultEnum.OVERDUE) {
+      userStore.setToken("");
+      router.replace(LOGIN_URL);
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    if (data.code && data.code !== ResultEnum.SUCCESS) {
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    return data;
+  },
+  error => {
+    ElMessage.error("请求失败,请稍后重试");
+    return Promise.reject(error);
+  }
+);
+
+// 审核状态:0=待审核 1=已通过 2=已驳回
+export const AUDIT_STATUS_LABEL: Record<number, string> = {
+  0: "待审核",
+  1: "已通过",
+  2: "已驳回"
+};
+
+// 在线状态:0=下线 1=上线
+export const ONLINE_STATUS_LABEL: Record<number, string> = {
+  0: "下线",
+  1: "上线"
+};
+
+// 演出类型:0=特邀演出 1=常规演出
+export const PERFORMANCE_TYPE_LABEL: Record<number, string> = {
+  0: "特邀演出",
+  1: "常规演出"
+};
+
+// 演出频次:"0"=单次 "1"=每天定时进行 "2"=每周定时进行
+export const FREQUENCY_LABEL: Record<string, string> = {
+  "0": "单次",
+  "1": "每天定时进行",
+  "2": "每周定时进行"
+};
+
+export interface PerformanceRow {
+  id: number | string;
+  name: string;
+  performanceType: number;
+  performanceTypeLabel: string;
+  frequency: string;
+  frequencyLabel: string;
+  shelfStatus: number;
+  onlineStatusLabel: string;
+  auditStatus: number | null;
+  auditStatusLabel: string;
+  submitTime: string;
+  auditReason: string;
+}
+
+export interface PerformanceListQuery {
+  page?: number;
+  pageNum?: number;
+  pageSize?: number;
+  size?: number;
+  storeId?: string | number;
+  performanceName?: string;
+  reviewStatus?: number;
+  onlineStatus?: number;
+  performanceType?: number;
+  submitTimeStart?: string;
+  submitTimeEnd?: string;
+}
+
+function transformListItem(item: any): PerformanceRow {
+  const performanceType = item.performanceType ?? 0;
+  const frequency = item.performanceFrequency ?? item.frequency ?? "0";
+  const auditStatus = item.reviewStatus ?? item.statusReview ?? item.auditStatus ?? 0;
+  const shelfStatus = item.onlineStatus ?? item.shelfStatus ?? 0;
+  return {
+    id: item.id,
+    name: item.performanceName ?? item.name ?? "",
+    performanceType: Number(performanceType),
+    performanceTypeLabel: PERFORMANCE_TYPE_LABEL[Number(performanceType)] ?? "--",
+    frequency: String(frequency),
+    frequencyLabel: FREQUENCY_LABEL[String(frequency)] ?? "单次",
+    shelfStatus: Number(shelfStatus),
+    onlineStatusLabel: shelfStatus === 1 ? "上线" : "--",
+    auditStatus,
+    auditStatusLabel: AUDIT_STATUS_LABEL[auditStatus] ?? "--",
+    submitTime: item.createdTime ?? item.submitTime ?? item.createTime ?? "--",
+    auditReason: item.rejectReason ?? item.rejectionReason ?? item.auditReason ?? ""
+  };
+}
+
+/** 分页查询演出列表 */
+export const getPerformancePage = (params: PerformanceListQuery) => {
+  const storeId = params.storeId ?? (localGet("createdId") as string) ?? "";
+  const page = params.page ?? params.pageNum ?? 1;
+  const size = params.size ?? params.pageSize ?? 10;
+  const query: any = {
+    page,
+    size,
+    storeId
+  };
+  if (params.performanceName != null && params.performanceName !== "") query.performanceName = params.performanceName;
+  if (params.reviewStatus !== undefined && params.reviewStatus !== null) query.reviewStatus = params.reviewStatus;
+  if (params.onlineStatus !== undefined && params.onlineStatus !== null) query.onlineStatus = params.onlineStatus;
+  if (params.performanceType !== undefined && params.performanceType !== null) query.performanceType = params.performanceType;
+  if (params.submitTimeStart) query.submitTimeStart = params.submitTimeStart;
+  if (params.submitTimeEnd) query.submitTimeEnd = params.submitTimeEnd;
+
+  return performanceAxios.get<any>(PORT_NONE + "/store/bar/performance/listByStoreId", { params: query });
+};
+
+/** 演出列表接口返回转 ProTable 所需 { list, total } */
+export const transformPerformanceListResponse = (payload: any) => {
+  const records = payload?.records ?? payload?.list ?? (Array.isArray(payload) ? payload : []);
+  const total = payload?.total ?? records.length;
+  return {
+    list: records.map((item: any) => transformListItem(item)),
+    total
+  };
+};
+
+/** 演出详情 */
+export const getPerformanceDetail = (id: number | string) => {
+  return performanceAxios.get<any>(PORT_NONE + "/store/bar/performance/detail", { params: { id } });
+};
+
+/** 新增/更新演出 */
+export const saveOrUpdatePerformance = (data: any) => {
+  return performanceAxios.post(PORT_NONE + "/store/bar/performance/saveOrUpdate", data);
+};
+
+/** 删除演出 */
+export const deletePerformance = (id: number | string) => {
+  return performanceAxios.delete(PORT_NONE + "/store/bar/performance/delete", { params: { id } });
+};
+
+/** 设置上线状态 */
+export const setPerformanceOnlineStatus = (id: number | string, onlineStatus: number) => {
+  return performanceAxios.post(PORT_NONE + "/store/bar/performance/setOnlineStatus", { id, onlineStatus });
+};
+
+/** 表演嘉宾列表(员工/嘉宾可选列表,用于新建编辑时选择) */
+export const getGuestListPage = (params: { pageNum?: number; pageSize?: number; storeId?: string; keyword?: string }) => {
+  const storeId = params.storeId ?? (localGet("createdId") as string) ?? "";
+  const pageNum = params.pageNum ?? 1;
+  const pageSize = params.pageSize ?? 20;
+  const query: any = { pageNum, pageSize, storeId };
+  if (params.keyword) query.keyword = params.keyword;
+  return performanceAxios.get<any>(PORT_NONE + "/storeStaffConfig/getStaffConfigList", { params: query });
+};
+
+/** 周几文本与数字映射 */
+const WEEKDAY_MAP: Record<string, number> = { 周一: 0, 周二: 1, 周三: 2, 周四: 3, 周五: 4, 周六: 5, 周日: 6 };
+const WEEKDAY_KEYS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
+
+/**
+ * 将表单数据转为后端 saveOrUpdate 所需格式(与商家端 dataConverter 一致)
+ */
+export function buildPerformanceBackendPayload(form: any, storeId?: string): any {
+  const sid = storeId ?? (localGet("createdId") as string) ?? "";
+  const payload: any = {
+    storeId: sid,
+    performanceName: form.name ?? "",
+    performancePoster: Array.isArray(form.posterUrls) ? form.posterUrls[0] : (form.imgUrl ?? ""),
+    performanceType: form.performanceType === "特邀演出" ? 0 : 1,
+    performanceFrequency: form.frequency === "单次" ? "0" : form.frequency === "每天定时进行" ? "1" : "2",
+    performanceStyle: Array.isArray(form.style) ? form.style.join(",") : form.style || "",
+    performanceContent: form.detailDescription ?? "",
+    performanceNotice: form.notes ?? "",
+    performanceDetail: Array.isArray(form.detailPicUrl) ? form.detailPicUrl.slice(0, 9).join(",") : (form.detailPicUrl ?? ""),
+    staffConfigIds:
+      Array.isArray(form.guests) && form.guests.length
+        ? form.guests
+            .map((g: any) => (typeof g === "object" ? g.id : g))
+            .filter(Boolean)
+            .join(",")
+        : ""
+  };
+  if (form.id) payload.id = form.id;
+
+  const fmtDt = (date: string, time: string) => (date && time ? `${date} ${time}:00` : "");
+
+  if (payload.performanceFrequency === "0") {
+    const pt = form.performanceTime || {};
+    payload.singleStartDatetime = fmtDt(pt.startDate, pt.startTime);
+    payload.singleEndDatetime = fmtDt(pt.endDate, pt.endTime);
+    payload.performanceWeek = "";
+  } else if (payload.performanceFrequency === "1") {
+    const dr = form.dailyPerformanceDateRange || {};
+    const tr = form.dailyPerformanceTimeRange || {};
+    payload.dailyStartDate = dr.start ?? "";
+    payload.dailyEndDate = dr.end ?? "";
+    payload.singleStartDatetime = tr.start ? `2000-01-01 ${tr.start}:00` : "";
+    payload.singleEndDatetime = tr.end ? `2000-01-01 ${tr.end}:00` : "";
+    payload.performanceWeek = "";
+  } else {
+    const dr = form.weeklyPerformanceDateRange || {};
+    const tr = form.weeklyPerformanceTimeRange || {};
+    const weekdays = form.weeklyPerformanceWeekdays || [];
+    payload.dailyStartDate = dr.start ?? "";
+    payload.dailyEndDate = dr.end ?? "";
+    payload.singleStartDatetime = tr.start ? `2000-01-01 ${tr.start}:00` : "";
+    payload.singleEndDatetime = tr.end ? `2000-01-01 ${tr.end}:00` : "";
+    payload.performanceWeek = weekdays
+      .map((d: string) => WEEKDAY_MAP[d])
+      .filter((n: number) => n !== undefined)
+      .sort((a: number, b: number) => a - b)
+      .join(",");
+  }
+  return payload;
+}
+
+/** 后端详情转表单模型(用于编辑回显) */
+export function detailToFormModel(d: any): Record<string, any> {
+  if (!d) return getDefaultFormModel();
+  const freq = d.performanceFrequency ?? "0";
+  const typeLabel = PERFORMANCE_TYPE_LABEL[Number(d.performanceType)] ?? "特邀演出";
+  const freqLabel = FREQUENCY_LABEL[String(freq)] ?? "单次";
+  const style = d.performanceStyle
+    ? String(d.performanceStyle)
+        .split(",")
+        .map((s: string) => s.trim())
+        .filter(Boolean)
+    : [];
+  const detailPicUrl = d.performanceDetail
+    ? String(d.performanceDetail)
+        .split(",")
+        .map((u: string) => u.trim())
+        .filter(Boolean)
+    : [];
+  let guests: any[] = [];
+  if (d.performers && Array.isArray(d.performers)) {
+    guests = d.performers.map((p: any) => ({
+      id: p.id,
+      name: p.name ?? "",
+      nickname: p.name ?? "",
+      avatar: p.avatar ?? "",
+      position: p.style ?? p.tag ?? ""
+    }));
+  } else if (d.staffConfigIds) {
+    guests = String(d.staffConfigIds)
+      .split(",")
+      .map(id => ({ id: id.trim(), name: "", position: "" }))
+      .filter((g: any) => g.id);
+  }
+  const model: Record<string, any> = {
+    id: d.id,
+    name: d.performanceName ?? "",
+    imgUrl: d.performancePoster ?? "",
+    posterUrls: d.performancePoster ? [d.performancePoster] : [],
+    style,
+    performanceType: typeLabel,
+    frequency: freqLabel,
+    detailDescription: d.performanceContent ?? "",
+    notes: d.performanceNotice ?? "",
+    detailPicUrl,
+    guests
+  };
+  if (freq === "0") {
+    let startDate = "",
+      startTime = "",
+      endDate = "",
+      endTime = "";
+    if (d.singleStartDatetime) {
+      const [sd, st] = String(d.singleStartDatetime).split(" ");
+      startDate = sd ?? "";
+      startTime = st ? st.substring(0, 5) : "";
+    }
+    if (d.singleEndDatetime) {
+      const [ed, et] = String(d.singleEndDatetime).split(" ");
+      endDate = ed ?? "";
+      endTime = et ? et.substring(0, 5) : "";
+    }
+    model.performanceTime = { startDate, startTime, endDate, endTime };
+  } else if (freq === "1") {
+    model.dailyPerformanceDateRange = { start: d.dailyStartDate ?? "", end: d.dailyEndDate ?? "" };
+    let startTime = "",
+      endTime = "";
+    if (d.singleStartDatetime) startTime = String(d.singleStartDatetime).split(" ")[1]?.substring(0, 5) ?? "";
+    if (d.singleEndDatetime) endTime = String(d.singleEndDatetime).split(" ")[1]?.substring(0, 5) ?? "";
+    model.dailyPerformanceTimeRange = { start: startTime, end: endTime };
+  } else {
+    model.weeklyPerformanceDateRange = { start: d.dailyStartDate ?? "", end: d.dailyEndDate ?? "" };
+    let startTime = "",
+      endTime = "";
+    if (d.singleStartDatetime) startTime = String(d.singleStartDatetime).split(" ")[1]?.substring(0, 5) ?? "";
+    if (d.singleEndDatetime) endTime = String(d.singleEndDatetime).split(" ")[1]?.substring(0, 5) ?? "";
+    model.weeklyPerformanceTimeRange = { start: startTime, end: endTime };
+    const weekStr = d.performanceWeek ?? "";
+    const nums = weekStr
+      ? weekStr
+          .split(",")
+          .map((n: string) => parseInt(n.trim(), 10))
+          .filter((n: number) => !isNaN(n))
+      : [];
+    const weekLabels: Record<number, string> = { 0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日" };
+    model.weeklyPerformanceWeekdays = nums.map((n: number) => weekLabels[n]).filter(Boolean);
+  }
+  return model;
+}
+
+export function getDefaultFormModel(): Record<string, any> {
+  return {
+    name: "",
+    imgUrl: "",
+    posterUrls: [],
+    style: ["民谣"],
+    performanceType: "特邀演出",
+    frequency: "单次",
+    performanceTime: { startDate: "", startTime: "", endDate: "", endTime: "" },
+    dailyPerformanceDateRange: { start: "", end: "" },
+    dailyPerformanceTimeRange: { start: "", end: "" },
+    weeklyPerformanceWeekdays: [],
+    weeklyPerformanceDateRange: { start: "", end: "" },
+    weeklyPerformanceTimeRange: { start: "", end: "" },
+    detailPicUrl: [],
+    detailDescription: "",
+    notes: "",
+    guests: []
+  };
+}

+ 8 - 0
src/api/modules/priceList.ts

@@ -119,6 +119,10 @@ export interface PriceListQuery {
   status?: number | string;
   shelfStatus?: number | string;
   businessSection?: string | number;
+  /** 提交时间起(提交时间段搜索) */
+  startCreatedTime?: string;
+  /** 提交时间止(提交时间段搜索) */
+  endCreatedTime?: string;
 }
 
 // 根据经营板块获取模块类型(与商家端 config.js 保持一致)
@@ -156,6 +160,8 @@ export const getPriceListPage = (params: PriceListQuery) => {
     if (params.status !== undefined && params.status !== null && params.status !== "") {
       query.status = params.status;
     }
+    if (params.startCreatedTime) query.startCreatedTime = params.startCreatedTime;
+    if (params.endCreatedTime) query.endCreatedTime = params.endCreatedTime;
 
     return priceListAxios.get<any>(PORT_NONE + "/store/cuisine/getPage", { params: query });
   }
@@ -174,6 +180,8 @@ export const getPriceListPage = (params: PriceListQuery) => {
   if (params.shelfStatus !== undefined && params.shelfStatus !== null && params.shelfStatus !== "") {
     query.shelfStatus = params.shelfStatus;
   }
+  if (params.startCreatedTime) query.startCreatedTime = params.startCreatedTime;
+  if (params.endCreatedTime) query.endCreatedTime = params.endCreatedTime;
 
   return priceListAxios.get<any>(PORT_NONE + "/store/price/getPage", { params: query });
 };

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

@@ -183,6 +183,37 @@
       ]
     },
     {
+      "path": "/performance",
+      "name": "performance",
+      "component": "/performance/index",
+      "meta": {
+        "icon": "VideoPlay",
+        "title": "演出",
+        "isLink": "",
+        "isHide": true,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/performance/edit",
+          "name": "performanceEdit",
+          "component": "/performance/edit",
+          "meta": {
+            "icon": "Menu",
+            "title": "新建/编辑演出",
+            "activeMenu": "/performance",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
+    },
+    {
       "path": "/orderManagement",
       "name": "orderManagement",
       "component": "/orderManagement/index",

+ 8 - 0
src/stores/modules/auth.ts

@@ -106,6 +106,14 @@ export const useAuthStore = defineStore({
                 // 有 storeId 时显示,无 storeId 时不显示
                 menu.meta.isHide = !storeId;
                 break;
+              case "价目表":
+                // 有店铺的商家才显示价目表
+                menu.meta.isHide = !storeId;
+                break;
+              case "演出":
+                // 有店铺且为休闲娱乐(businessSection=2) 时显示演出
+                menu.meta.isHide = !storeId || businessSection !== 2;
+                break;
             }
           }
 

+ 210 - 408
src/views/home/components/go-flow.vue

@@ -31,7 +31,7 @@
               list-type="picture-card"
               :limit="1"
               :on-exceed="handleExceed"
-              :on-success="(response, file) => handleUploadSuccess(response, file, 'ID_CARD')"
+              :on-success="(response, file) => handleUploadSuccess(response, file, 'ID_CARD', '身份证')"
               :on-preview="handlePictureCardPreview"
               :on-remove="handleRemove"
               accept="image/*"
@@ -55,7 +55,7 @@
               list-type="picture-card"
               :limit="1"
               :on-exceed="handleExceed"
-              :on-success="(response, file) => handleUploadSuccess(response, file, 'ID_CARD')"
+              :on-success="(response, file) => handleUploadSuccess(response, file, 'ID_CARD', '身份证')"
               :on-preview="handlePictureCardPreview"
               :on-remove="handleRemove"
               accept="image/*"
@@ -136,7 +136,7 @@
               </el-form-item>
 
               <el-form-item label="经营板块" prop="businessSection">
-                <el-radio-group v-model="step2Form.businessSection" @change="changeBusinessSector">
+                <el-radio-group v-model="step2Form.businessSection" @change="changeBusinessSection">
                   <el-radio
                     v-for="businessSection in businessSectionList"
                     :value="businessSection.dictId"
@@ -147,39 +147,32 @@
                 </el-radio-group>
               </el-form-item>
 
-              <!-- 如果有二级分类数据,显示经营种类 -->
-              <el-form-item label="经营种类" prop="businessTypes" v-if="secondLevelList.length > 0">
-                <el-radio-group v-model="step2Form.businessTypes" @change="changeBusinessSecondLevel">
-                  <el-radio v-for="item in secondLevelList" :value="item.dictId" :key="item.dictId">
-                    {{ item.dictDetail }}
+              <el-form-item label="标签" prop="storeTickets" v-if="showDisportLicence">
+                <el-radio-group v-model="step2Form.storeTickets">
+                  <el-radio
+                    v-for="businessSection in businessLabelList"
+                    :value="businessSection.dictId"
+                    :key="businessSection.dictId"
+                  >
+                    {{ businessSection.dictDetail }}
                   </el-radio>
                 </el-radio-group>
               </el-form-item>
 
-              <!-- 只有当有三级分类数据且已选中经营种类时,才显示分类(三级分类多选) -->
-              <el-form-item
-                label="分类"
-                prop="businessSecondLevel"
-                v-if="thirdLevelList && thirdLevelList.length > 0 && step2Form.businessTypes"
-              >
-                <el-checkbox-group v-model="step2Form.businessSecondLevel">
-                  <el-checkbox v-for="item in thirdLevelList" :key="item.dictId" :label="item.dictDetail" :value="item.dictId" />
-                </el-checkbox-group>
+              <!-- 经营种类 -->
+              <el-form-item label="经营种类" prop="businessTypeName">
+                <el-input v-model="step2Form.businessTypeName" type="textarea" :rows="3" placeholder="请输入" maxlength="255" />
               </el-form-item>
 
-              <!-- <el-form-item label="分类" prop="businessSecondLevel">
-                <el-radio-group v-model="step2Form.businessSecondLevel" @change="changeBusinessSecondLevel">
-                  <el-radio v-for="item in secondLevelList" :value="item.dictId" :key="item.dictId">
-                    {{ item.dictDetail }}
-                  </el-radio>
-                </el-radio-group>
-              </el-form-item> -->
-              <el-form-item label="是否提供餐食" prop="businessSecondMeal" v-if="showMealOption">
-                <el-radio-group v-model="step2Form.businessSecondMeal" @change="changeBusinessSecondMeal">
-                  <el-radio v-for="item in secondMealList" :value="item.value" :key="item.key">
-                    {{ item.dictDetail }}
-                  </el-radio>
-                </el-radio-group>
+              <!-- 经营类目 -->
+              <el-form-item label="经营类目" prop="businessCategoryName">
+                <el-input
+                  v-model="step2Form.businessCategoryName"
+                  type="textarea"
+                  :rows="3"
+                  placeholder="请输入"
+                  maxlength="255"
+                />
               </el-form-item>
             </div>
 
@@ -215,6 +208,15 @@
                 </el-select>
               </el-form-item>
 
+              <!-- 店铺评价(三项绑定 storePj 数组,校验才能通过) -->
+              <el-form-item label="店铺评价" prop="storePj" required>
+                <div class="store-pj-inputs">
+                  <el-input v-model="step2Form.storePj[0]" placeholder="请输入" maxlength="2" clearable class="store-pj-input" />
+                  <el-input v-model="step2Form.storePj[1]" maxlength="2" placeholder="请输入" clearable class="store-pj-input" />
+                  <el-input v-model="step2Form.storePj[2]" placeholder="请输入" maxlength="2" clearable class="store-pj-input" />
+                </div>
+              </el-form-item>
+
               <el-form-item label="营业执照" prop="businessLicenseAddress">
                 <el-upload
                   v-model:file-list="step2Form.businessLicenseAddress"
@@ -232,55 +234,19 @@
                 </el-upload>
               </el-form-item>
 
-              <!-- 合同图片上传表单项 - 已隐藏 (2026-01-17) -->
-              <!-- <el-form-item label="合同图片" prop="contractImageList">
-                <el-upload
-                  v-model:file-list="step2Form.contractImageList"
-                  :http-request="handleHttpUpload"
-                  list-type="picture-card"
-                  :limit="20"
-                  :on-exceed="handleExceed"
-                  :on-success="(response, file) => handleUploadSuccess(response, file, '', '合同图片')"
-                  :on-preview="handlePictureCardPreview"
-                >
-                  <el-icon><Plus /></el-icon>
-                  <template #tip>
-                    <div class="el-upload__tip">({{ step2Form.contractImageList.length }}/20)</div>
-                  </template>
-                </el-upload>
-              </el-form-item> -->
-
-              <!-- 食品经营许可证上传表单项 - 已隐藏 (2026-01-17) -->
-              <!-- <el-form-item label="食品经营许可证" prop="foodLicenceImgList" v-if="showFoodLicence">
-                <el-upload
-                  v-model:file-list="step2Form.foodLicenceImgList"
-                  :http-request="handleHttpUpload"
-                  list-type="picture-card"
-                  :limit="1"
-                  :on-exceed="handleExceed"
-                  :on-success="(response, file) => handleUploadSuccess(response, file, 'FOOD_MANAGE_LICENSE', '食品经营许可证')"
-                  :on-preview="handlePictureCardPreview"
-                >
-                  <el-icon><Plus /></el-icon>
-                  <template #tip>
-                    <div class="el-upload__tip">({{ step2Form.foodLicenceImgList.length }}/1)</div>
-                  </template>
-                </el-upload>
-              </el-form-item> -->
-
-              <el-form-item label="其他资质证明" prop="disportLicenceImgList" v-if="showDisportLicence">
+              <el-form-item label="其他资质证明" prop="disportLicenceImgList">
                 <el-upload
                   v-model:file-list="step2Form.disportLicenceImgList"
                   :http-request="handleHttpUpload"
+                  :limit="20"
                   list-type="picture-card"
-                  :limit="1"
                   :on-exceed="handleExceed"
                   :on-success="(response, file) => handleUploadSuccess(response, file, 'BUSINESS_LICENSE', '其他资质证明')"
                   :on-preview="handlePictureCardPreview"
                 >
                   <el-icon><Plus /></el-icon>
                   <template #tip>
-                    <div class="el-upload__tip">({{ step2Form.disportLicenceImgList.length }}/1)</div>
+                    <div class="el-upload__tip">已上传 {{ step2Form.disportLicenceImgList.length }} 张(最多20张)</div>
                   </template>
                 </el-upload>
               </el-form-item>
@@ -307,15 +273,7 @@
 
 <script setup lang="ts">
 import { ref, reactive, watch, onMounted, computed } from "vue";
-import {
-  ElMessage,
-  ElMessageBox,
-  type FormInstance,
-  type FormRules,
-  UploadProps,
-  UploadUserFile,
-  UploadRequestOptions
-} from "element-plus";
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules, UploadUserFile, UploadRequestOptions } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 
 import {
@@ -333,7 +291,7 @@ import { useAuthStore } from "@/stores/modules/auth";
 const authStore = useAuthStore();
 const userInfo = localGet("geeker-user")?.userInfo || {};
 const latShow = ref(false);
-
+const showDisportLicence = ref(false);
 // 图片预览相关
 const imageViewerVisible = ref(false);
 const imageViewerUrlList = ref<string[]>([]);
@@ -353,19 +311,35 @@ const entryList = ref([
     title: "入驻成功"
   }
 ]);
+const businessLabelList = ref<any[]>([
+  {
+    dictId: 1,
+    dictDetail: "装修公司"
+  },
+  {
+    dictId: 2,
+    dictDetail: "其他类型"
+  }
+]);
 
+const businessSectionList = ref<any[]>([
+  {
+    dictId: 1,
+    dictDetail: "特色美食"
+  },
+  {
+    dictId: 2,
+    dictDetail: "休闲娱乐"
+  },
+  {
+    dictId: 3,
+    dictDetail: "生活服务"
+  }
+]);
 // 身份证正反面上传列表
 const idCardFrontList = ref<UploadUserFile[]>([]);
 const idCardBackList = ref<UploadUserFile[]>([]);
-//
-const showFoodLicence = ref(true);
-const changeBusinessSecondMeal = (value: any) => {
-  if (value == 0) {
-    showFoodLicence.value = false;
-  } else {
-    showFoodLicence.value = true;
-  }
-};
+
 // OCR 识别结果
 const ocrResult = ref<{
   name?: string;
@@ -420,135 +394,50 @@ const isIdCardUploadComplete = computed(() => {
   return idCardFrontList.value.length > 0 && idCardBackList.value.length > 0;
 });
 
-// 计算是否需要显示其他资质证明(酒吧或KTV时显示)
-const showDisportLicence = computed(() => {
-  const sectionName = step2Form.businessSectionName;
-  const sectionId = step2Form.businessSection;
-
-  // 通过名称判断(最直接的方式)
-  if (sectionName === "酒吧" || sectionName === "KTV" || sectionName === "ktv") {
-    return true;
-  }
-
-  // 通过查找 businessSectionList 来判断(兼容 id 和 dictId)
-  if (businessSectionList.value.length > 0 && sectionId) {
-    const selectedSection = businessSectionList.value.find((item: any) => {
-      // 匹配 id 或 dictId
-      const isMatchId =
-        item.id === sectionId ||
-        item.dictId === sectionId ||
-        String(item.id) === String(sectionId) ||
-        String(item.dictId) === String(sectionId);
-      // 匹配名称
-      const isMatchName = item.dictDetail === "酒吧" || item.dictDetail === "KTV" || item.dictDetail === "ktv";
-      return isMatchId && isMatchName;
-    });
-
-    if (selectedSection) {
-      return true;
-    }
-  }
-
-  // 通过 dictId 判断(KTV 的 dictId 通常是 "3")
-  if (String(sectionId) === "3") {
-    return true;
-  }
-
-  return false;
-});
-
-// 计算是否需要显示"是否提供餐食"(KTV、按摩足疗、丽人美发、运动健身、洗浴汗蒸时显示)
-const showMealOption = computed(() => {
-  const sectionName = step2Form.businessSectionName;
-  const sectionId = step2Form.businessSection;
-
-  // 需要显示的经营板块名称列表
-  const mealSectionNames = ["KTV", "ktv", "按摩足疗", "丽人美发", "运动健身", "洗浴汗蒸"];
-
-  // 通过名称判断(最直接的方式)
-  if (mealSectionNames.includes(sectionName)) {
-    return true;
-  }
-
-  // 通过查找 businessSectionList 来判断(兼容 id 和 dictId)
-  if (businessSectionList.value.length > 0 && sectionId) {
-    const selectedSection = businessSectionList.value.find((item: any) => {
-      // 匹配 id 或 dictId
-      const isMatchId =
-        item.dictId === sectionId ||
-        item.dictId === sectionId ||
-        String(item.dictId) === String(sectionId) ||
-        String(item.dictId) === String(sectionId);
-      // 匹配名称
-      const isMatchName = mealSectionNames.includes(item.dictDetail);
-      return isMatchId && isMatchName;
-    });
-
-    if (selectedSection) {
-      return true;
-    }
-  }
-
-  // 通过 dictId 判断(KTV=3, 按摩足疗=5, 丽人美发=6, 运动健身=7, 洗浴汗蒸=4)
-  const mealSectionIds = ["3", "4", "5", "6", "7"];
-  if (mealSectionIds.includes(String(sectionId))) {
-    return true;
-  }
-
-  return false;
-});
-
 // 下一步 - 验证身份证正反面是否已上传
 const handleNextStep = async () => {
   // 识别成功,进入下一步
-  try {
-    const res: any = await verifyIdInfo({
-      idCard: ocrResult.value.idCard,
-      name: ocrResult.value.name,
-      appType: 1
-    });
-    if (res.code === 200) {
-      ElMessage.success("身份证识别成功");
-      setStep(2);
-    } else {
-      ElMessage.error(res?.msg || "身份证识别失败");
-    }
-  } catch (error: any) {
-    console.log(error);
-  }
+  // try {
+  //   const res: any = await verifyIdInfo({
+  //     idCard: ocrResult.value.idCard,
+  //     name: ocrResult.value.name,
+  //     appType: 1
+  //   });
+  //   if (res.code === 200) {
+  //     ElMessage.success("身份证识别成功");
+  //     setStep(2);
+  //   } else {
+  //     ElMessage.error(res?.msg || "身份证识别失败");
+  //   }
+  // } catch (error: any) {
+  //   console.log(error);
+  // }
+  setStep(2);
 };
-
-const secondMealList = ref([
-  { key: 1, value: 1, dictDetail: "提供" },
-  { key: 0, value: 0, dictDetail: "不提供" }
-]);
 const step2Rules: FormRules = {
   storeName: [{ required: true, message: "请输入店铺名称", trigger: "blur" }],
   storeCapacity: [{ required: true, message: "请输入容纳人数", trigger: "blur" }],
   storeArea: [{ required: true, message: "请选择门店面积", trigger: "change" }],
   storeBlurb: [{ required: true, message: "请输入门店简介", trigger: "change" }],
-  storeIntro: [{ required: true, message: "请输入门店简介", trigger: "blur" }],
   businessSection: [{ required: true, message: "请选择经营板块", trigger: "change" }],
-  businessSecondLevel: [{ required: true, message: "请选择分类", trigger: "change" }],
-  businessSecondMeal: [{ required: true, message: "请选择是否提供餐食", trigger: "change" }],
-  businessTypes: [
+  storeTickets: [{ required: true, message: "请选择标签", trigger: "change" }],
+  businessTypeName: [{ required: true, message: "请输入经营种类", trigger: "change" }],
+  businessCategoryName: [{ required: true, message: "请选择经营类目", trigger: "change" }],
+  address: [{ required: true, message: "请输入经纬度", trigger: "blur" }],
+  businessLicenseAddress: [{ required: true, message: "请上传营业执照", trigger: "change" }],
+  storePj: [
     {
       required: true,
-      message: "请选择经营种类",
-      trigger: "change",
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.length === 0) {
-          callback(new Error("请选择经营种类"));
-        } else {
-          callback();
-        }
+      message: "请完整填写三项店铺评价",
+      trigger: "blur",
+      validator: (_rule: any, value: any, callback: (err?: Error) => void) => {
+        const list = Array.isArray(value) ? value : [];
+        const filled = list.length >= 3 && list.every(v => v != null && String(v).trim() !== "");
+        if (filled) callback();
+        else callback(new Error("请完整填写三项店铺评价"));
       }
     }
   ],
-  address: [{ required: true, message: "请输入经纬度", trigger: "blur" }],
-  businessLicenseAddress: [{ required: true, message: "请上传营业执照", trigger: "change" }],
-  // contractImageList: [{ required: true, message: "请上传合同图片", trigger: "change" }], // 已隐藏 (2026-01-17)
-  // foodLicenceImgList: [{ required: true, message: "请上传食品经营许可证", trigger: "change" }], // 已隐藏 (2026-01-17)
   disportLicenceImgList: [{ required: true, message: "请上传其他资质证明", trigger: "change" }]
 };
 
@@ -591,6 +480,14 @@ watch(
   }
 );
 
+const changeBusinessSection = () => {
+  if (step2Form.businessSection == 3) {
+    showDisportLicence.value = true;
+  } else {
+    showDisportLicence.value = false;
+    step2Form.storeTickets = "";
+  }
+};
 // 隐藏财务管理菜单的函数
 const hideFinancialManagementMenu = () => {
   const hideMenus = (menuList: any[]) => {
@@ -676,7 +573,6 @@ watch(
 );
 
 onMounted(() => {
-  getBusinessSectionList();
   callGetUserInfo();
   if (currentStep.value === 3 && (storeApplicationStatus.value === 0 || storeApplicationStatus.value === 2)) {
     updateStoreIdInCache();
@@ -708,8 +604,8 @@ const step2Form = reactive({
   administrativeRegionDistrictAdcode: "",
   storeAddress: "",
   storeBlurb: "",
-  businessSection: "1",
-  businessSectionName: "",
+  businessSection: 1,
+  storeTickets: "" as string,
   businessSecondLevel: [] as string[],
   businessTypes: "" as string,
   businessTypesList: [] as string[],
@@ -722,7 +618,13 @@ const step2Form = reactive({
   contractImageList: [] as UploadUserFile[],
   foodLicenceImgList: [] as UploadUserFile[],
   disportLicenceImgList: [] as UploadUserFile[],
-  address: ""
+  address: "",
+  // 经营种类
+  businessTypeName: "",
+  // 经营类目
+  businessCategoryName: "",
+  // 店铺评价(三项,与 prop="storePj" 对应,校验规则会检查此数组)
+  storePj: ["", "", ""] as string[]
 });
 
 // 返回按钮
@@ -768,89 +670,11 @@ watch(
   }
 );
 
-//经营板块 - 一级分类
-const businessSectionList = ref<any[]>([]);
-const getBusinessSectionList = async () => {
-  try {
-    const res: any = await getFirstLevelList({});
-    if (res && res.code === 200 && res.data) {
-      businessSectionList.value = res.data;
-      // 如果有数据,自动加载第一个经营板块的二级分类(经营种类)
-      if (res.data && res.data.length > 0) {
-        const firstSection = res.data[0];
-        const firstDictId = firstSection.dictId || firstSection.id;
-        if (firstDictId) {
-          try {
-            const secondRes: any = await getSecondLevelList({ parentDictId: String(firstDictId) });
-            if (secondRes && (secondRes.code === 200 || secondRes.code === "200") && secondRes.data) {
-              secondLevelList.value = secondRes.data;
-            }
-          } catch (error) {
-            console.error("获取二级分类失败:", error);
-          }
-        }
-      }
-    }
-  } catch (error) {
-    console.error("获取一级分类失败:", error);
-    ElMessage.error("获取经营板块失败");
-  }
-};
-
 // 二级分类列表
 const secondLevelList = ref<any[]>([]);
 // 三级分类列表
 const thirdLevelList = ref<any[]>([]);
 
-// 一级分类变化时,加载二级分类
-const changeBusinessSector = async (dictId: string | number | boolean | undefined) => {
-  const dictIdStr = String(dictId || "");
-  if (!dictIdStr) {
-    secondLevelList.value = [];
-    thirdLevelList.value = [];
-    step2Form.businessSecondLevel = [];
-    step2Form.businessTypes = "";
-    step2Form.businessTypesList = [];
-    step2Form.businessSection = "";
-    step2Form.businessSectionName = "";
-    return;
-  }
-
-  // 更新一级分类信息(模板中 :value 绑定的是 dictId,所以这里查找时优先使用 dictId)
-  const selectedSection = businessSectionList.value.find(
-    (item: any) => String(item.dictId) === dictIdStr || String(item.id) === dictIdStr
-  );
-
-  if (selectedSection) {
-    // 保持与模板中 :value="businessSection.dictId" 绑定一致,使用 dictId
-    step2Form.businessSection = String(selectedSection.dictId || selectedSection.id);
-    step2Form.businessSectionName = selectedSection.dictDetail;
-  } else {
-    // 如果没找到,直接使用传入的值
-    step2Form.businessSection = dictIdStr;
-    step2Form.businessSectionName = "";
-  }
-
-  // 清空二级和三级分类
-  secondLevelList.value = [];
-  thirdLevelList.value = [];
-  step2Form.businessSecondLevel = [];
-  step2Form.businessTypes = "";
-  step2Form.businessTypesList = [];
-
-  // 加载二级分类(使用 dictId 作为 parentDictId)
-  const parentDictId = selectedSection?.dictId || dictIdStr;
-  try {
-    const res: any = await getSecondLevelList({ parentDictId });
-    if (res && (res.code === 200 || res.code === "200") && res.data) {
-      secondLevelList.value = res.data;
-    }
-  } catch (error) {
-    console.error("获取二级分类失败:", error);
-    ElMessage.error("获取经营种类失败");
-  }
-};
-
 // 二级分类变化时,加载三级分类
 const changeBusinessSecondLevel = async (dictId: string | number | boolean | undefined) => {
   const dictIdStr = String(dictId || "");
@@ -899,7 +723,7 @@ const getLonAndLat = async (keyword: string) => {
   }
 };
 
-const selectAddress = async (param: any) => {
+const selectAddress = async () => {
   if (!step2Form.address || typeof step2Form.address !== "string") {
     ElMessage.warning("地址格式不正确,请重新选择");
     return;
@@ -1104,7 +928,6 @@ const autoOcrRecognition = async (ocrType: string, name: string) => {
           const validToDate = res.data[0]?.validToDate || "";
           if (validToDate) {
             entertainmentLicenceExpirationTime.value = formatDate(validToDate);
-            console.log(entertainmentLicenceExpirationTime.value);
           }
         }
 
@@ -1120,9 +943,7 @@ const autoOcrRecognition = async (ocrType: string, name: string) => {
         if (res.data && Array.isArray(res.data) && res.data.length > 0) {
           const validToDate = res.data[0]?.validToDate || "";
           if (validToDate) {
-            console.log(formatDate(validToDate));
             foodLicenceExpirationTime.value = formatDate(validToDate);
-            console.log(foodLicenceExpirationTime.value);
           }
         }
         foodLicenseOcrStatus.value = "success";
@@ -1132,22 +953,28 @@ const autoOcrRecognition = async (ocrType: string, name: string) => {
       console.warn("OCR 识别失败:", res?.msg);
       if (name === "营业执照") {
         businessLicenseOcrStatus.value = "failed";
+        step2Form.businessLicenseAddress = [];
       } else if (name === "其他资质证明") {
         entertainmentLicenseOcrStatus.value = "failed";
+        step2Form.disportLicenceImgList = [];
       } else if (ocrType === "FOOD_MANAGE_LICENSE") {
         foodLicenseOcrStatus.value = "failed";
+        step2Form.foodLicenceImgList = [];
       }
-      ElMessage.error(res?.msg || "识别失败,请重试");
+      ElMessage.error(res?.msg || "识别失败,已清除该图片,请重新上传");
     }
   } catch (error) {
     if (name === "营业执照") {
       businessLicenseOcrStatus.value = "failed";
+      step2Form.businessLicenseAddress = [];
     } else if (name === "其他资质证明") {
       entertainmentLicenseOcrStatus.value = "failed";
+      step2Form.disportLicenceImgList = [];
     } else if (ocrType === "FOOD_MANAGE_LICENSE") {
       foodLicenseOcrStatus.value = "failed";
+      step2Form.foodLicenceImgList = [];
     }
-    ElMessage.error("识别失败,请重试");
+    ElMessage.error("识别失败,已清除该图片,请重新上传");
   } finally {
     isOcrProcessing.value = false;
   }
@@ -1158,12 +985,16 @@ const handleUploadSuccess = (response: any, uploadFile: UploadUserFile, ocrType:
   if (response?.fileUrl) {
     uploadFile.url = response.fileUrl;
   }
-  // 只有指定的类型才进行 OCR 识别
+
+  // 其他资质证明不做 OCR,直接返回
+  if (name === "其他资质证明") {
+    return;
+  }
+
+  // 仅对身份证、营业执照、食品许可证做 OCR
   if (ocrType && (ocrType === "ID_CARD" || ocrType === "BUSINESS_LICENSE" || ocrType === "FOOD_MANAGE_LICENSE")) {
     if (name === "营业执照") {
       businessLicenseOcrStatus.value = "none";
-    } else if (name === "其他资质证明") {
-      entertainmentLicenseOcrStatus.value = "none";
     } else if (ocrType === "FOOD_MANAGE_LICENSE") {
       foodLicenseOcrStatus.value = "none";
     }
@@ -1282,7 +1113,8 @@ const buildWhereAddress = async (regionCodes: string[]) => {
   }
   return whereAddress;
 };
-const handleAi = async () => {
+// 店铺入驻成功后调用 AI 审核接口(参考商家端:后台调用,不阻塞、不向用户展示结果)
+const handleAi = () => {
   const businessLicenseUrls = getFileUrls(step2Form.businessLicenseAddress);
   const contractImageUrls = getFileUrls(step2Form.contractImageList);
   const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList);
@@ -1291,20 +1123,17 @@ const handleAi = async () => {
   const licenseImages = [...businessLicenseUrls, ...contractImageUrls, ...foodLicenceUrls, ...disportLicenceUrls].filter(Boolean);
 
   const params: any = {
-    business_scope: step2Form.storeBlurb,
+    business_scope: step2Form.storeBlurb || "",
     contact_email: "",
-    contact_name: userInfo.name,
-    contact_phone: userInfo.phone,
+    contact_name: userInfo.name || "",
+    contact_phone: userInfo.phone || "",
     license_images: licenseImages,
-    merchant_name: step2Form.storeName,
-    userId: userInfo.id
+    merchant_name: step2Form.storeName || "",
+    userId: userInfo.id || ""
   };
-  const res: any = await getAiapprovestoreInfo(params);
-  if (res.code == 200) {
-    ElMessage.success(res.msg || "AI智能审核成功");
-  } else {
-    ElMessage.error(res?.msg || "AI智能审核失败");
-  }
+  getAiapprovestoreInfo(params).catch(aiError => {
+    console.error("AI店铺审核接口调用失败:", aiError);
+  });
 };
 // 提交
 const handleSubmit = async () => {
@@ -1312,139 +1141,100 @@ const handleSubmit = async () => {
 
   await step2FormRef.value.validate(async valid => {
     if (valid) {
-      // 检查OCR识别状态,如果有识别失败的证照,提示用户重新上传
-      const failedLicenses: string[] = [];
-      if (step2Form.businessLicenseAddress.length > 0 && businessLicenseOcrStatus.value === "failed") {
-        failedLicenses.push("营业执照");
-      }
-      if (step2Form.foodLicenceImgList.length > 0 && foodLicenseOcrStatus.value === "failed") {
-        failedLicenses.push("食品经营许可证");
-      }
-      if (step2Form.disportLicenceImgList.length > 0 && entertainmentLicenseOcrStatus.value === "failed") {
-        failedLicenses.push("其他资质证明");
-      }
-
-      if (failedLicenses.length > 0) {
-        ElMessage.warning("请重新上传证照");
-        return;
-      }
       const businessLicenseUrls = getFileUrls(step2Form.businessLicenseAddress);
-      // const contractImageUrls = getFileUrls(step2Form.contractImageList); // 已隐藏 (2026-01-17)
-      // const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList); // 已隐藏 (2026-01-17)
-      const contractImageUrls: string[] = []; // 空数组替代 (2026-01-17)
-      const foodLicenceUrls: string[] = []; // 空数组替代 (2026-01-17)
+      const contractImageUrls = getFileUrls(step2Form.contractImageList);
+      const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList);
       const disportLicenceUrls = getFileUrls(step2Form.disportLicenceImgList);
 
       const whereAddress = await buildWhereAddress(step2Form.region);
 
-      let storeStatus = 1;
-      if (step2Form.businessType === "正常营业") {
-        storeStatus = 1;
-      } else if (step2Form.businessType === "暂停营业") {
-        storeStatus = 0;
-      } else if (step2Form.businessType === "筹建中") {
-        storeStatus = 2;
-      }
-
-      const storeAreaNum = typeof step2Form.storeArea === "string" ? parseInt(step2Form.storeArea) : step2Form.storeArea;
-
-      const addressObj = {
-        address: queryAddress.value || "",
-        longitude: parseFloat(step2Form.storePositionLongitude) || 0,
-        latitude: parseFloat(step2Form.storePositionLatitude) || 0
-      };
-
       const storePosition =
         step2Form.storePositionLongitude && step2Form.storePositionLatitude
           ? `${step2Form.storePositionLongitude},${step2Form.storePositionLatitude}`
           : "";
+      const storePositionLatitude = parseFloat(step2Form.storePositionLatitude) || 0;
+      const storePositionLongitude = parseFloat(step2Form.storePositionLongitude) || 0;
 
-      let fullStoreAddress = "";
-      if (whereAddress.length > 0) {
-        const provinceName = whereAddress[0]?.name || "";
-        const cityName = whereAddress[1]?.name || "";
-        const districtName = whereAddress[2]?.name || "";
-        fullStoreAddress = `${provinceName}${cityName}${districtName}`;
-      }
-
-      // 获取身份证正反面URL
-      const idCardFrontUrl = getFileUrls(idCardFrontList.value)[0] || "";
-      const idCardBackUrl = getFileUrls(idCardBackList.value)[0] || "";
-
-      // 处理经营种类和三级分类
-      // 根据用户需求:如果有三级分类,传经营板块、经营种类(二级分类)、三级分类
-      // 如果没有三级分类,传经营板块、经营种类(二级分类)
       let finalBusinessTypes: string[] = [];
-      let businessClassifyList: string[] = [];
-
       if (thirdLevelList.value.length > 0) {
-        // 有三级分类
-        // businessTypes 存储三级分类的 dictId(字符串)
         if (step2Form.businessTypes) {
           finalBusinessTypes = [step2Form.businessTypes];
-          businessClassifyList = [step2Form.businessTypes];
         }
-        // 如果有三级分类,还需要传递二级分类(经营种类)
-        if (Array.isArray(step2Form.businessSecondLevel) && step2Form.businessSecondLevel.length > 0) {
-          // 如果 finalBusinessTypes 为空,使用二级分类
-          if (finalBusinessTypes.length === 0) {
-            finalBusinessTypes = step2Form.businessSecondLevel;
-          }
+        if (
+          Array.isArray(step2Form.businessSecondLevel) &&
+          step2Form.businessSecondLevel.length > 0 &&
+          finalBusinessTypes.length === 0
+        ) {
+          finalBusinessTypes = step2Form.businessSecondLevel;
         }
       } else {
-        // 没有三级分类,使用二级分类(经营种类)
         if (Array.isArray(step2Form.businessSecondLevel) && step2Form.businessSecondLevel.length > 0) {
           finalBusinessTypes = step2Form.businessSecondLevel;
         } else if (step2Form.businessTypes) {
-          // 如果没有选择二级分类,使用 businessTypes(可能是二级分类的 dictId)
           finalBusinessTypes = [step2Form.businessTypes];
         } else if (step2Form.businessTypesList.length > 0) {
-          // 使用旧的逻辑
           finalBusinessTypes = step2Form.businessTypesList;
         }
-        // 没有三级分类时,businessClassifyList 保持为空数组
       }
 
-      const params = {
-        foodLicenceExpirationTime: foodLicenceExpirationTime.value, //食品经营许可证到期时间
-        entertainmentLicenceExpirationTime: entertainmentLicenceExpirationTime.value, //其他资质证明到期时间
-        businessClassifyList: step2Form.businessSecondLevel, //三级分类
-        mealProvided: step2Form.businessSecondMeal, //是否提供餐食
-        entertainmentLicenseAddress: disportLicenceUrls, //其他资质证明
+      const businessTypesListVal = finalBusinessTypes.length > 0 ? finalBusinessTypes[0] : "";
+      // const categoryListVal = step2Form.businessCategoryName ? [step2Form.businessCategoryName] : [];
+
+      // 与商家端 storeInfo saveStoreInfo 入参保持一致(storeInfoDto + 提交覆盖项)
+      const storeInfoDto: any = {
+        updatedTime: null,
         storeTel: userInfo.phone,
         storeName: step2Form.storeName,
         storeCapacity: step2Form.storeCapacity,
-        storeArea: storeAreaNum,
+        storeArea: typeof step2Form.storeArea === "string" ? parseInt(step2Form.storeArea) : step2Form.storeArea,
         isChain: step2Form.isChain,
-        // storeDetailAddress: step2Form.storeDetailAddress,
         storeAddress: step2Form.storeDetailAddress,
         storeBlurb: step2Form.storeBlurb,
-        businessSection: step2Form.businessSection,
-        businessTypesList: finalBusinessTypes,
-        storeStatus: storeStatus,
-        businessStatus: step2Form.businessStatus,
-        address: addressObj,
+        businessSection: String(step2Form.businessSection),
+        businessSectionName:
+          step2Form.businessSection == 1 ? "特色美食" : step2Form.businessSection == 2 ? "休闲娱乐" : "生活服务",
+        businessTypeName: step2Form.businessTypeName,
+        storeStatus: step2Form.businessType === "暂停营业" ? 0 : step2Form.businessType === "筹建中" ? 2 : 1,
+        queryAddress: queryAddress.value || "",
+        storePosition,
+        storePositionLatitude,
+        storePositionLongitude,
+        // businessTypesList: [businessTypesListVal],
+        // businessTypes: businessTypesListVal,
+        businessClassify: step2Form.businessCategoryName,
         businessLicenseAddress: businessLicenseUrls,
+        businessLicenseUrl: businessLicenseUrls.join(","),
         contractImageList: contractImageUrls,
-        foodLicenceImgList: foodLicenceUrls,
-        disportLicenceUrls: disportLicenceUrls,
-        // storeAddress: fullStoreAddress,
-        whereAddress: whereAddress,
-        updatedTime: null,
-        queryAddress: queryAddress.value,
-        storePosition: storePosition,
-        storePositionLatitude: parseFloat(step2Form.storePositionLatitude) || 0,
-        storePositionLongitude: parseFloat(step2Form.storePositionLongitude) || 0,
-        businessSectionName: step2Form.businessSectionName,
-        businessTypes: finalBusinessTypes,
-        foodLicenceUrl: foodLicenceUrls.length > 0 ? foodLicenceUrls[0] : "",
+        foodLicenceUrl: foodLicenceUrls,
+        otherQualificationImages: disportLicenceUrls,
+        otherLicenses: disportLicenceUrls.join(","),
+        storeEvaluate: (step2Form.storePj || []).join(","),
+        evaluation1: step2Form.storePj[0],
+        evaluation2: step2Form.storePj[1],
+        evaluation3: step2Form.storePj[2],
         userAccount: userInfo.id,
-        administrativeRegionProvinceAdcode: step2Form.administrativeRegionProvinceAdcode,
-        administrativeRegionCityAdcode: step2Form.administrativeRegionCityAdcode,
-        administrativeRegionDistrictAdcode: step2Form.administrativeRegionDistrictAdcode,
-        idCardFrontUrl: idCardFrontUrl,
-        idCardBackUrl: idCardBackUrl
+        administrativeRegionProvinceAdcode: step2Form.administrativeRegionProvinceAdcode || whereAddress[0]?.adcode || "",
+        administrativeRegionProvinceName: whereAddress[0]?.name || "",
+        administrativeRegionCityAdcode: step2Form.administrativeRegionCityAdcode || whereAddress[1]?.adcode || "",
+        administrativeRegionCityName: whereAddress[1]?.name || "",
+        administrativeRegionDistrictAdcode: step2Form.administrativeRegionDistrictAdcode || whereAddress[2]?.adcode || "",
+        administrativeRegionDistrictName: whereAddress[2]?.name || "",
+        businessStatus: step2Form.businessStatus,
+        storeContact: userInfo.name || (localGet("smName") as string) || "",
+        idCard: localGet("idCard") || ""
+      };
+
+      if (String(step2Form.businessSection) === "3") {
+        storeInfoDto.storeTickets = step2Form.storeTickets;
+      }
+
+      const saveStoreInfoParams = {
+        ...storeInfoDto,
+        foodLicenceUrl: foodLicenceUrls.join(","),
+        mealsFlag: step2Form.businessSecondMeal === 1 ? 1 : 0,
+        createdUserId: userInfo.id || ""
       };
+
       ElMessageBox.confirm("确认提交入驻申请吗?", "提示", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
@@ -1452,23 +1242,24 @@ const handleSubmit = async () => {
       })
         .then(async () => {
           try {
-            const res: any = await applyStore(params);
+            const res: any = await applyStore(saveStoreInfoParams);
             if (res && res.code == 200) {
               storeApplicationStatus.value = 0;
-              ElMessage.success(res.msg);
+              ElMessage.success(res.msg || "提交成功");
+              if (res.data?.id) {
+                localSet("createdId", res.data.id);
+              }
               callGetUserInfo();
               setStep(0);
               handleAi();
             } else {
-              ElMessage.error(res.msg || "提交失败");
+              ElMessage.error(res?.msg || "提交失败");
             }
           } catch (error) {
             ElMessage.error("提交失败,请重试");
           }
         })
-        .catch(() => {
-          // 取消提交
-        });
+        .catch(() => {});
     } else {
       ElMessage.error("请完善表单信息");
     }
@@ -1482,6 +1273,17 @@ const handleExceed = () => {
 </script>
 
 <style scoped lang="scss">
+// 店铺评价三个输入框纵向排列
+.store-pj-inputs {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 100%;
+  .store-pj-input {
+    width: 100%;
+  }
+}
+
 // 表单页面样式
 .form-container {
   min-height: calc(100vh - 100px);

+ 2 - 0
src/views/home/index.vue

@@ -127,6 +127,8 @@ const getUserInfo = async () => {
         const resStore: any = await getDetail(param1);
         if (resStore && resStore.code == 200 && resStore.data) {
           storeApplicationStatus.value = resStore.data.storeApplicationStatus;
+          // 有店铺时重新拉取菜单,使价目表、演出等“有店铺才显示”的菜单正确展示
+          authStore.getAuthMenuList();
           // 如果是等待审核(0)或审核拒绝(2),且当前步骤为0,显示 go-enter 组件
           // 如果用户已经主动跳转到其他步骤,则不重置步骤
           if ((storeApplicationStatus.value == 0 || storeApplicationStatus.value == 2) && currentStep.value === 0) {

+ 135 - 0
src/views/performance/components/GuestSelectDialog.vue

@@ -0,0 +1,135 @@
+<template>
+  <el-dialog v-model="visible" title="表演嘉宾" width="640px" destroy-on-close @closed="onClosed">
+    <div class="guest-search">
+      <el-input v-model="keyword" placeholder="昵称/姓名" clearable style="width: 200px; margin-right: 8px" />
+      <el-button type="primary" @click="search"> 搜索 </el-button>
+      <el-button @click="resetSearch"> 重置 </el-button>
+    </div>
+    <el-table ref="tableRef" :data="guestList" v-loading="loading" max-height="360" @selection-change="onSelectionChange">
+      <el-table-column type="selection" width="50" />
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column label="头像" width="80" align="center">
+        <template #default="{ row }">
+          <el-avatar :size="40" :src="row.avatar || row.staffImage">
+            <el-icon><User /></el-icon>
+          </el-avatar>
+        </template>
+      </el-table-column>
+      <el-table-column prop="name" label="姓名" min-width="100">
+        <template #default="{ row }">
+          {{ row.name || row.nickname || row.staffName || "--" }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="position" label="职位" min-width="100">
+        <template #default="{ row }">
+          {{ row.position || row.staffPosition || "--" }}
+        </template>
+      </el-table-column>
+    </el-table>
+    <template #footer>
+      <el-button @click="visible = false"> 取消 </el-button>
+      <el-button type="primary" @click="confirm"> 确定 </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+import { User } from "@element-plus/icons-vue";
+import { localGet } from "@/utils";
+import type { ElTable } from "element-plus";
+
+export interface GuestItem {
+  id: number | string;
+  name?: string;
+  nickname?: string;
+  avatar?: string;
+  position?: string;
+  staffName?: string;
+  staffImage?: string;
+  staffPosition?: string;
+}
+
+const props = defineProps<{
+  modelValue: boolean;
+  selectedIds?: (string | number)[];
+}>();
+
+const emit = defineEmits<{
+  (e: "update:modelValue", v: boolean): void;
+  (e: "confirm", list: GuestItem[]): void;
+}>();
+
+const visible = ref(props.modelValue);
+const keyword = ref("");
+const loading = ref(false);
+const guestList = ref<GuestItem[]>([]);
+const selectedRows = ref<GuestItem[]>([]);
+const tableRef = ref<InstanceType<typeof ElTable>>();
+
+watch(
+  () => props.modelValue,
+  v => {
+    visible.value = v;
+    if (v) loadList();
+  },
+  { immediate: true }
+);
+watch(visible, v => emit("update:modelValue", v));
+
+async function loadList() {
+  loading.value = true;
+  try {
+    const { getGuestListPage } = await import("@/api/modules/performance");
+    const res: any = await getGuestListPage({
+      storeId: localGet("createdId") as string,
+      pageNum: 1,
+      pageSize: 100,
+      keyword: keyword.value || undefined
+    });
+    const raw = res?.data ?? res;
+    const list = raw?.records ?? raw?.list ?? (Array.isArray(raw) ? raw : []);
+    guestList.value = list.map((item: any) => ({
+      id: item.id ?? item.staffConfigId,
+      name: item.name ?? item.nickname ?? item.staffName,
+      nickname: item.nickname ?? item.name ?? item.staffName,
+      avatar: item.avatar ?? item.staffImage ?? item.headImg,
+      position: item.position ?? item.staffPosition ?? item.style
+    }));
+  } catch (e) {
+    guestList.value = [];
+  } finally {
+    loading.value = false;
+  }
+}
+
+function search() {
+  loadList();
+}
+
+function resetSearch() {
+  keyword.value = "";
+  loadList();
+}
+
+function onSelectionChange(rows: GuestItem[]) {
+  selectedRows.value = rows;
+}
+
+function confirm() {
+  emit("confirm", selectedRows.value);
+  visible.value = false;
+}
+
+function onClosed() {
+  keyword.value = "";
+  guestList.value = [];
+  selectedRows.value = [];
+}
+</script>
+
+<style scoped lang="scss">
+.guest-search {
+  margin-bottom: 12px;
+}
+</style>

+ 111 - 0
src/views/performance/components/PerformanceDetailDialog.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="详情" width="800px" destroy-on-close @close="handleClose">
+    <div v-loading="loading" class="detail-body">
+      <template v-if="detail">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="演出名称">
+            {{ detail.name || "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="类型">
+            {{ detail.performanceTypeLabel ?? "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="频次">
+            {{ detail.frequencyLabel ?? "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="在线状态">
+            {{ detail.onlineStatusLabel ?? "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="审核状态">
+            {{ detail.auditStatusLabel ?? "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="提交时间">
+            {{ detail.submitTime ?? "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item label="审核时间" :span="2">
+            {{ detail.auditTime ?? "--" }}
+          </el-descriptions-item>
+          <el-descriptions-item v-if="detail.auditStatus === 2" label="拒绝原因" :span="2">
+            <div class="reject-reason-text">
+              {{ detail.auditReason || "--" }}
+            </div>
+          </el-descriptions-item>
+        </el-descriptions>
+      </template>
+      <el-empty v-else-if="!loading" description="暂无数据" />
+    </div>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+import { getPerformanceDetail } from "@/api/modules/performance";
+import { AUDIT_STATUS_LABEL, PERFORMANCE_TYPE_LABEL, FREQUENCY_LABEL, type PerformanceRow } from "@/api/modules/performance";
+
+const props = defineProps<{
+  modelValue: boolean;
+  performanceId: string | number | null;
+}>();
+
+const emit = defineEmits<{
+  (e: "update:modelValue", v: boolean): void;
+}>();
+
+const dialogVisible = ref(props.modelValue);
+const loading = ref(false);
+const detail = ref<(PerformanceRow & { auditTime?: string }) | null>(null);
+
+watch(
+  () => props.modelValue,
+  v => {
+    dialogVisible.value = v;
+    if (v && props.performanceId) fetchDetail();
+    else detail.value = null;
+  },
+  { immediate: true }
+);
+
+watch(dialogVisible, v => emit("update:modelValue", v));
+
+async function fetchDetail() {
+  if (!props.performanceId) return;
+  loading.value = true;
+  detail.value = null;
+  try {
+    const res: any = await getPerformanceDetail(props.performanceId);
+    const raw = res?.data ?? res;
+    if (!raw) return;
+    const d = raw;
+    detail.value = {
+      id: d.id,
+      name: d.performanceName ?? d.name ?? "",
+      performanceType: d.performanceType ?? 0,
+      performanceTypeLabel: PERFORMANCE_TYPE_LABEL[Number(d.performanceType)] ?? "--",
+      frequency: d.performanceFrequency ?? "0",
+      frequencyLabel: FREQUENCY_LABEL[String(d.performanceFrequency)] ?? "单次",
+      shelfStatus: d.onlineStatus ?? d.shelfStatus ?? 0,
+      onlineStatusLabel: (d.onlineStatus ?? d.shelfStatus) === 1 ? "上线" : "--",
+      auditStatus: d.reviewStatus ?? d.auditStatus ?? 0,
+      auditStatusLabel: AUDIT_STATUS_LABEL[d.reviewStatus ?? d.auditStatus] ?? "--",
+      submitTime: d.createdTime ?? d.submitTime ?? d.createTime ?? "--",
+      auditReason: d.rejectReason ?? d.auditReason ?? "",
+      auditTime: d.auditTime ?? d.reviewTime ?? "--"
+    };
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleClose() {
+  emit("update:modelValue", false);
+}
+</script>
+
+<style scoped lang="scss">
+.detail-body {
+  min-height: 120px;
+}
+.reject-reason-text {
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+</style>

+ 560 - 0
src/views/performance/components/PerformanceFormDialog.vue

@@ -0,0 +1,560 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="performanceId ? '编辑' : '新建/编辑'"
+    width="900px"
+    destroy-on-close
+    class="performance-form-dialog"
+    @closed="onClosed"
+  >
+    <el-form ref="formRef" :model="form" :rules="formRules" label-width="120px" @submit.prevent>
+      <el-row :gutter="24">
+        <el-col :span="12">
+          <el-form-item label="演出名称" prop="name" required>
+            <el-input v-model="form.name" placeholder="请输入" maxlength="20" show-word-limit clearable />
+          </el-form-item>
+          <el-form-item label="演出海报" required>
+            <div class="upload-wrap">
+              <UploadImgs
+                v-model:file-list="posterFileList"
+                :api="uploadImgStore"
+                :limit="1"
+                :file-size="5"
+                class="poster-upload"
+                @update:file-list="onPosterChange"
+              />
+              <span class="upload-tip">({{ posterFileList.length }}/1)</span>
+            </div>
+          </el-form-item>
+          <el-form-item label="演出风格" required>
+            <el-checkbox-group v-model="form.style">
+              <el-checkbox v-for="opt in styleOptions" :key="opt" :label="opt">
+                {{ opt }}
+              </el-checkbox>
+            </el-checkbox-group>
+          </el-form-item>
+          <el-form-item label="演出类型">
+            <el-radio-group v-model="form.performanceType">
+              <el-radio value="特邀演出"> 特邀演出 </el-radio>
+              <el-radio value="常规演出"> 常规演出 </el-radio>
+            </el-radio-group>
+            <div class="form-tip">特邀演出为单次</div>
+          </el-form-item>
+          <el-form-item label="演出频次">
+            <el-radio-group v-model="form.frequency">
+              <el-radio value="单次"> 单次 </el-radio>
+              <el-radio value="每天定时进行" :disabled="form.performanceType === '特邀演出'"> 每天定时进行 </el-radio>
+              <el-radio value="每周定时进行" :disabled="form.performanceType === '特邀演出'"> 每周定时进行 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <!-- 单次:演出时间 -->
+          <template v-if="form.frequency === '单次'">
+            <el-form-item label="演出时间" required>
+              <div class="date-time-row">
+                <el-date-picker
+                  v-model="singleStartDate"
+                  type="date"
+                  placeholder="年/月/日"
+                  value-format="YYYY-MM-DD"
+                  style="width: 140px"
+                />
+                <el-time-picker
+                  v-model="singleStartTime"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="--:--"
+                  style="width: 100px"
+                />
+                <span class="sep">至</span>
+                <el-date-picker
+                  v-model="singleEndDate"
+                  type="date"
+                  placeholder="年/月/日"
+                  value-format="YYYY-MM-DD"
+                  style="width: 140px"
+                />
+                <el-time-picker
+                  v-model="singleEndTime"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="--:--"
+                  style="width: 100px"
+                />
+              </div>
+            </el-form-item>
+          </template>
+          <!-- 每天:演出日期 + 演出时间 -->
+          <template v-if="form.frequency === '每天定时进行'">
+            <el-form-item label="演出日期" required>
+              <div class="date-time-row">
+                <el-date-picker
+                  v-model="dailyDateStart"
+                  type="date"
+                  placeholder="年/月/日"
+                  value-format="YYYY-MM-DD"
+                  style="width: 140px"
+                />
+                <span class="sep">至</span>
+                <el-date-picker
+                  v-model="dailyDateEnd"
+                  type="date"
+                  placeholder="年/月/日"
+                  value-format="YYYY-MM-DD"
+                  style="width: 140px"
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="演出时间" required>
+              <div class="date-time-row">
+                <el-time-picker
+                  v-model="dailyTimeStart"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="--:--"
+                  style="width: 100px"
+                />
+                <span class="sep">至</span>
+                <el-time-picker
+                  v-model="dailyTimeEnd"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="--:--"
+                  style="width: 100px"
+                />
+              </div>
+            </el-form-item>
+          </template>
+          <!-- 每周:演出日期(周几) + 演出日期(范围) + 演出时间 -->
+          <template v-if="form.frequency === '每周定时进行'">
+            <el-form-item label="演出日期">
+              <el-checkbox-group v-model="form.weeklyPerformanceWeekdays">
+                <el-checkbox v-for="day in weekdays" :key="day" :label="day">
+                  {{ day }}
+                </el-checkbox>
+              </el-checkbox-group>
+            </el-form-item>
+            <el-form-item label="演出日期" required>
+              <div class="date-time-row">
+                <el-date-picker
+                  v-model="weeklyDateStart"
+                  type="date"
+                  placeholder="年/月/日"
+                  value-format="YYYY-MM-DD"
+                  style="width: 140px"
+                />
+                <span class="sep">至</span>
+                <el-date-picker
+                  v-model="weeklyDateEnd"
+                  type="date"
+                  placeholder="年/月/日"
+                  value-format="YYYY-MM-DD"
+                  style="width: 140px"
+                />
+              </div>
+            </el-form-item>
+            <el-form-item label="演出时间" required>
+              <div class="date-time-row">
+                <el-time-picker
+                  v-model="weeklyTimeStart"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="--:--"
+                  style="width: 100px"
+                />
+                <span class="sep">至</span>
+                <el-time-picker
+                  v-model="weeklyTimeEnd"
+                  format="HH:mm"
+                  value-format="HH:mm"
+                  placeholder="--:--"
+                  style="width: 100px"
+                />
+              </div>
+            </el-form-item>
+          </template>
+
+          <el-form-item label="图文详情图片">
+            <div class="upload-wrap">
+              <UploadImgs
+                v-model:file-list="detailImageFileList"
+                :api="uploadImgStore"
+                :limit="9"
+                :file-size="5"
+                class="detail-upload"
+                @update:file-list="onDetailImagesChange"
+              />
+              <span class="upload-tip">({{ form.detailPicUrl.length }}/9)</span>
+            </div>
+          </el-form-item>
+          <el-form-item label="图文详情描述">
+            <el-input
+              v-model="form.detailDescription"
+              type="textarea"
+              placeholder="请输入"
+              :rows="3"
+              maxlength="300"
+              show-word-limit
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="表演嘉宾">
+            <el-link type="primary" @click="openGuestSelect"> 请添加 </el-link>
+            <div v-if="form.guests.length" class="guest-list">
+              <div v-for="g in form.guests" :key="String(g.id)" class="guest-item">
+                <el-avatar :size="32" :src="g.avatar">
+                  <el-icon><User /></el-icon>
+                </el-avatar>
+                <span class="guest-name">{{ g.name || g.nickname || "--" }} {{ g.position ? ` ${g.position}` : "" }}</span>
+                <el-icon class="guest-remove" @click="removeGuest(g)">
+                  <Close />
+                </el-icon>
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item label="演出须知">
+            <el-input v-model="form.notes" type="textarea" placeholder="请输入" :rows="6" maxlength="300" show-word-limit />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button @click="visible = false"> 取消 </el-button>
+      <el-button type="primary" :loading="submitting" @click="submit"> 提交 </el-button>
+    </template>
+    <GuestSelectDialog v-model="guestSelectVisible" :selected-ids="selectedGuestIds" @confirm="onGuestsConfirm" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, computed } from "vue";
+import { ElMessage } from "element-plus";
+import { User, Close } from "@element-plus/icons-vue";
+import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
+import UploadImgs from "@/components/Upload/Imgs.vue";
+import { uploadImgStore } from "@/api/modules/upload";
+import {
+  getPerformanceDetail,
+  saveOrUpdatePerformance,
+  detailToFormModel,
+  getDefaultFormModel,
+  buildPerformanceBackendPayload
+} from "@/api/modules/performance";
+import GuestSelectDialog from "./GuestSelectDialog.vue";
+import type { GuestItem } from "./GuestSelectDialog.vue";
+
+const styleOptions = [
+  "民谣",
+  "爵士",
+  "摇滚",
+  "电音",
+  "流行",
+  "蓝调",
+  "AXAPPELLA (全人声伴奏)",
+  "SOUL",
+  "朋克",
+  "R&B",
+  "EDM",
+  "原创",
+  "古典",
+  "金属",
+  "说唱"
+];
+const weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
+
+const props = defineProps<{
+  modelValue: boolean;
+  performanceId: string | number | null;
+  mode: "add" | "edit";
+}>();
+
+const emit = defineEmits<{
+  (e: "update:modelValue", v: boolean): void;
+  (e: "success"): void;
+}>();
+
+const visible = ref(props.modelValue);
+const formRef = ref<FormInstance>();
+const submitting = ref(false);
+const posterFileList = ref<UploadUserFile[]>([]);
+const detailImageFileList = ref<UploadUserFile[]>([]);
+const guestSelectVisible = ref(false);
+
+const form = ref<Record<string, any>>(getDefaultFormModel());
+
+watch(
+  () => props.modelValue,
+  v => {
+    visible.value = v;
+    if (v) {
+      if (props.performanceId) loadDetail();
+      else Object.assign(form.value, getDefaultFormModel());
+      syncPosterAndDetailFiles();
+    }
+  },
+  { immediate: true }
+);
+watch(visible, v => emit("update:modelValue", v));
+watch(
+  () => form.value.performanceType,
+  type => {
+    if (type === "特邀演出") form.value.frequency = "单次";
+  }
+);
+
+// 单次时间绑定到 form.performanceTime
+const singleStartDate = computed({
+  get: () => form.value.performanceTime?.startDate ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.startDate = v;
+  }
+});
+const singleStartTime = computed({
+  get: () => form.value.performanceTime?.startTime ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.startTime = v;
+  }
+});
+const singleEndDate = computed({
+  get: () => form.value.performanceTime?.endDate ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.endDate = v;
+  }
+});
+const singleEndTime = computed({
+  get: () => form.value.performanceTime?.endTime ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.endTime = v;
+  }
+});
+const dailyDateStart = computed({
+  get: () => form.value.dailyPerformanceDateRange?.start ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceDateRange) form.value.dailyPerformanceDateRange = {};
+    form.value.dailyPerformanceDateRange.start = v;
+  }
+});
+const dailyDateEnd = computed({
+  get: () => form.value.dailyPerformanceDateRange?.end ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceDateRange) form.value.dailyPerformanceDateRange = {};
+    form.value.dailyPerformanceDateRange.end = v;
+  }
+});
+const dailyTimeStart = computed({
+  get: () => form.value.dailyPerformanceTimeRange?.start ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceTimeRange) form.value.dailyPerformanceTimeRange = {};
+    form.value.dailyPerformanceTimeRange.start = v;
+  }
+});
+const dailyTimeEnd = computed({
+  get: () => form.value.dailyPerformanceTimeRange?.end ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceTimeRange) form.value.dailyPerformanceTimeRange = {};
+    form.value.dailyPerformanceTimeRange.end = v;
+  }
+});
+const weeklyDateStart = computed({
+  get: () => form.value.weeklyPerformanceDateRange?.start ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceDateRange) form.value.weeklyPerformanceDateRange = {};
+    form.value.weeklyPerformanceDateRange.start = v;
+  }
+});
+const weeklyDateEnd = computed({
+  get: () => form.value.weeklyPerformanceDateRange?.end ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceDateRange) form.value.weeklyPerformanceDateRange = {};
+    form.value.weeklyPerformanceDateRange.end = v;
+  }
+});
+const weeklyTimeStart = computed({
+  get: () => form.value.weeklyPerformanceTimeRange?.start ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceTimeRange) form.value.weeklyPerformanceTimeRange = {};
+    form.value.weeklyPerformanceTimeRange.start = v;
+  }
+});
+const weeklyTimeEnd = computed({
+  get: () => form.value.weeklyPerformanceTimeRange?.end ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceTimeRange) form.value.weeklyPerformanceTimeRange = {};
+    form.value.weeklyPerformanceTimeRange.end = v;
+  }
+});
+
+const selectedGuestIds = computed(() => form.value.guests?.map((g: any) => g.id) ?? []);
+
+const formRules: FormRules = {
+  name: [{ required: true, message: "请输入演出名称", trigger: "blur" }]
+};
+
+async function loadDetail() {
+  if (!props.performanceId) return;
+  try {
+    const res: any = await getPerformanceDetail(props.performanceId);
+    const raw = res?.data ?? res;
+    if (raw) Object.assign(form.value, detailToFormModel(raw));
+    syncPosterAndDetailFiles();
+  } catch (e) {
+    ElMessage.error("获取详情失败");
+  }
+}
+
+function syncPosterAndDetailFiles() {
+  const poster = form.value.posterUrls?.[0] ?? form.value.imgUrl;
+  posterFileList.value = poster ? ([{ name: "poster", url: poster }] as UploadUserFile[]) : [];
+  const urls = form.value.detailPicUrl ?? [];
+  detailImageFileList.value = urls.map((u: string, i: number) => ({ name: `detail-${i}`, url: u })) as UploadUserFile[];
+}
+
+function onPosterChange(list: UploadUserFile[]) {
+  const url = list.length ? ((list[0].url as string) ?? "") : "";
+  form.value.posterUrls = url ? [url] : [];
+  form.value.imgUrl = url;
+}
+
+function onDetailImagesChange(list: UploadUserFile[]) {
+  form.value.detailPicUrl = list.map(f => (typeof f.url === "string" ? f.url : "")).filter(Boolean);
+}
+
+function openGuestSelect() {
+  guestSelectVisible.value = true;
+}
+
+function onGuestsConfirm(list: GuestItem[]) {
+  form.value.guests = list;
+}
+
+function removeGuest(g: any) {
+  form.value.guests = (form.value.guests || []).filter((x: any) => x.id !== g.id);
+}
+
+function onClosed() {
+  form.value = getDefaultFormModel();
+  posterFileList.value = [];
+  detailImageFileList.value = [];
+}
+
+function validateForm(): boolean {
+  if (!form.value.name?.trim()) {
+    ElMessage.warning("请输入演出名称");
+    return false;
+  }
+  const poster = form.value.posterUrls?.[0] ?? form.value.imgUrl;
+  if (!poster) {
+    ElMessage.warning("请上传演出海报");
+    return false;
+  }
+  if (!form.value.style?.length) {
+    ElMessage.warning("请选择演出风格");
+    return false;
+  }
+  if (form.value.frequency === "单次") {
+    const pt = form.value.performanceTime || {};
+    if (!pt.startDate || !pt.startTime || !pt.endDate || !pt.endTime) {
+      ElMessage.warning("请填写演出时间");
+      return false;
+    }
+  }
+  if (form.value.frequency === "每天定时进行") {
+    const dr = form.value.dailyPerformanceDateRange || {};
+    const tr = form.value.dailyPerformanceTimeRange || {};
+    if (!dr.start || !dr.end || !tr.start || !tr.end) {
+      ElMessage.warning("请填写演出日期与时间");
+      return false;
+    }
+  }
+  if (form.value.frequency === "每周定时进行") {
+    const wr = form.value.weeklyPerformanceDateRange || {};
+    const tr = form.value.weeklyPerformanceTimeRange || {};
+    if (!wr.start || !wr.end || !tr.start || !tr.end) {
+      ElMessage.warning("请填写演出日期与时间");
+      return false;
+    }
+  }
+  return true;
+}
+
+async function submit() {
+  if (!validateForm() || submitting.value) return;
+  submitting.value = true;
+  try {
+    const payload = buildPerformanceBackendPayload(form.value);
+    const res: any = await saveOrUpdatePerformance(payload);
+    if (res?.code === 200) {
+      ElMessage.success(props.performanceId ? "编辑成功" : "新建成功");
+      emit("success");
+      visible.value = false;
+    } else {
+      ElMessage.error(res?.msg || "提交失败");
+    }
+  } catch (e) {
+    ElMessage.error("提交失败");
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.upload-wrap {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  .poster-upload,
+  .detail-upload {
+    :deep(.el-upload-list--picture-card) {
+      --el-upload-list-picture-card-size: 100px;
+    }
+    :deep(.el-upload--picture-card) {
+      width: 100px;
+      height: 100px;
+    }
+  }
+  .upload-tip {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+.form-tip {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+}
+.date-time-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  .sep {
+    color: #909399;
+  }
+}
+.guest-list {
+  margin-top: 8px;
+  .guest-item {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    padding: 6px 0;
+    .guest-name {
+      flex: 1;
+      font-size: 14px;
+    }
+    .guest-remove {
+      color: #909399;
+      cursor: pointer;
+      &:hover {
+        color: #f56c6c;
+      }
+    }
+  }
+}
+</style>

+ 752 - 0
src/views/performance/edit.vue

@@ -0,0 +1,752 @@
+<template>
+  <div class="performance-edit-page">
+    <div class="page-header">
+      <div class="header-left" @click="goBack">
+        <el-icon class="back-icon">
+          <ArrowLeft />
+        </el-icon>
+        <span class="back-text">返回</span>
+      </div>
+      <h1 class="page-title">
+        {{ viewMode ? "详情" : id ? "编辑" : "新建" }}
+      </h1>
+      <el-button v-if="viewMode" type="primary" @click="goEdit"> 编辑 </el-button>
+      <div v-else class="header-right" @click="goBack">
+        <el-icon class="close-icon">
+          <Close />
+        </el-icon>
+      </div>
+    </div>
+
+    <div class="page-content">
+      <el-form
+        ref="formRef"
+        :model="form"
+        :rules="formRules"
+        label-width="120px"
+        class="performance-form two-column-form"
+        @submit.prevent
+      >
+        <el-row :gutter="24">
+          <el-col :span="12">
+            <el-form-item label="演出名称" prop="name" required>
+              <el-input
+                v-model="form.name"
+                placeholder="请输入"
+                maxlength="20"
+                show-word-limit
+                clearable
+                :disabled="viewMode"
+                class="form-input"
+              />
+            </el-form-item>
+            <el-form-item label="演出海报" required>
+              <div class="upload-wrap">
+                <UploadImgs
+                  v-model:file-list="posterFileList"
+                  :api="uploadImgStore"
+                  :limit="1"
+                  :file-size="5"
+                  :disabled="viewMode"
+                  class="poster-upload"
+                  @update:file-list="onPosterChange"
+                />
+                <span class="upload-tip">({{ posterFileList.length }}/1)</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="演出风格" required>
+              <el-checkbox-group v-model="form.style" :disabled="viewMode">
+                <el-checkbox v-for="opt in styleOptions" :key="opt" :label="opt">
+                  {{ opt }}
+                </el-checkbox>
+              </el-checkbox-group>
+            </el-form-item>
+            <el-form-item label="演出类型">
+              <el-radio-group v-model="form.performanceType" :disabled="viewMode">
+                <el-radio value="特邀演出"> 特邀演出 </el-radio>
+                <el-radio value="常规演出"> 常规演出 </el-radio>
+              </el-radio-group>
+              <div class="form-tip">特邀演出为单次</div>
+            </el-form-item>
+            <el-form-item label="演出频次">
+              <el-radio-group v-model="form.frequency" :disabled="viewMode">
+                <el-radio value="单次"> 单次 </el-radio>
+                <el-radio value="每天定时进行" :disabled="form.performanceType === '特邀演出'"> 每天定时进行 </el-radio>
+                <el-radio value="每周定时进行" :disabled="form.performanceType === '特邀演出'"> 每周定时进行 </el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <template v-if="form.frequency === '单次'">
+              <el-form-item label="演出时间" required>
+                <div class="date-time-row">
+                  <el-date-picker
+                    v-model="singleStartDate"
+                    type="date"
+                    placeholder="年/月/日"
+                    value-format="YYYY-MM-DD"
+                    style="width: 140px"
+                    :disabled="viewMode"
+                  />
+                  <el-time-picker
+                    v-model="singleStartTime"
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    placeholder="--:--"
+                    style="width: 100px"
+                    :disabled="viewMode"
+                  />
+                  <span class="sep">至</span>
+                  <el-date-picker
+                    v-model="singleEndDate"
+                    type="date"
+                    placeholder="年/月/日"
+                    value-format="YYYY-MM-DD"
+                    style="width: 140px"
+                    :disabled="viewMode"
+                  />
+                  <el-time-picker
+                    v-model="singleEndTime"
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    placeholder="--:--"
+                    style="width: 100px"
+                    :disabled="viewMode"
+                  />
+                </div>
+              </el-form-item>
+            </template>
+            <template v-if="form.frequency === '每天定时进行'">
+              <el-form-item label="演出日期" required>
+                <div class="date-time-row">
+                  <el-date-picker
+                    v-model="dailyDateStart"
+                    type="date"
+                    placeholder="年/月/日"
+                    value-format="YYYY-MM-DD"
+                    style="width: 140px"
+                    :disabled="viewMode"
+                  />
+                  <span class="sep">至</span>
+                  <el-date-picker
+                    v-model="dailyDateEnd"
+                    type="date"
+                    placeholder="年/月/日"
+                    value-format="YYYY-MM-DD"
+                    style="width: 140px"
+                    :disabled="viewMode"
+                  />
+                </div>
+              </el-form-item>
+              <el-form-item label="演出时间" required>
+                <div class="date-time-row">
+                  <el-time-picker
+                    v-model="dailyTimeStart"
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    placeholder="--:--"
+                    style="width: 100px"
+                    :disabled="viewMode"
+                  />
+                  <span class="sep">至</span>
+                  <el-time-picker
+                    v-model="dailyTimeEnd"
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    placeholder="--:--"
+                    style="width: 100px"
+                    :disabled="viewMode"
+                  />
+                </div>
+              </el-form-item>
+            </template>
+            <template v-if="form.frequency === '每周定时进行'">
+              <el-form-item label="演出日期">
+                <el-checkbox-group v-model="form.weeklyPerformanceWeekdays" :disabled="viewMode">
+                  <el-checkbox v-for="day in weekdays" :key="day" :label="day">
+                    {{ day }}
+                  </el-checkbox>
+                </el-checkbox-group>
+              </el-form-item>
+              <el-form-item label="演出日期" required>
+                <div class="date-time-row">
+                  <el-date-picker
+                    v-model="weeklyDateStart"
+                    type="date"
+                    placeholder="年/月/日"
+                    value-format="YYYY-MM-DD"
+                    style="width: 140px"
+                    :disabled="viewMode"
+                  />
+                  <span class="sep">至</span>
+                  <el-date-picker
+                    v-model="weeklyDateEnd"
+                    type="date"
+                    placeholder="年/月/日"
+                    value-format="YYYY-MM-DD"
+                    style="width: 140px"
+                    :disabled="viewMode"
+                  />
+                </div>
+              </el-form-item>
+              <el-form-item label="演出时间" required>
+                <div class="date-time-row">
+                  <el-time-picker
+                    v-model="weeklyTimeStart"
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    placeholder="--:--"
+                    style="width: 100px"
+                    :disabled="viewMode"
+                  />
+                  <span class="sep">至</span>
+                  <el-time-picker
+                    v-model="weeklyTimeEnd"
+                    format="HH:mm"
+                    value-format="HH:mm"
+                    placeholder="--:--"
+                    style="width: 100px"
+                    :disabled="viewMode"
+                  />
+                </div>
+              </el-form-item>
+            </template>
+
+            <el-form-item label="图文详情图片">
+              <div class="upload-wrap">
+                <UploadImgs
+                  v-model:file-list="detailImageFileList"
+                  :api="uploadImgStore"
+                  :limit="9"
+                  :file-size="5"
+                  :disabled="viewMode"
+                  class="detail-upload"
+                  @update:file-list="onDetailImagesChange"
+                />
+                <span class="upload-tip">({{ form.detailPicUrl.length }}/9)</span>
+              </div>
+            </el-form-item>
+            <el-form-item label="图文详情描述">
+              <el-input
+                v-model="form.detailDescription"
+                type="textarea"
+                placeholder="请输入"
+                :rows="3"
+                maxlength="300"
+                show-word-limit
+                :disabled="viewMode"
+                class="form-textarea"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="表演嘉宾">
+              <el-link v-if="!viewMode" type="primary" @click="openGuestSelect"> 请添加 </el-link>
+              <div v-if="form.guests.length" class="guest-list">
+                <div v-for="g in form.guests" :key="String(g.id)" class="guest-item">
+                  <el-avatar :size="32" :src="g.avatar">
+                    <el-icon><User /></el-icon>
+                  </el-avatar>
+                  <span class="guest-name">{{ g.name || g.nickname || "--" }} {{ g.position ? ` ${g.position}` : "" }}</span>
+                  <el-icon v-if="!viewMode" class="guest-remove" @click="removeGuest(g)">
+                    <Close />
+                  </el-icon>
+                </div>
+              </div>
+            </el-form-item>
+            <el-form-item label="演出须知">
+              <el-input
+                v-model="form.notes"
+                type="textarea"
+                placeholder="请输入"
+                :rows="6"
+                maxlength="300"
+                show-word-limit
+                :disabled="viewMode"
+                class="form-textarea"
+              />
+            </el-form-item>
+            <!-- 详情模式:只读状态信息 -->
+            <template v-if="viewMode">
+              <el-form-item label="提交时间">
+                <span class="view-only-value">{{ detailExtra.submitTime || "--" }}</span>
+              </el-form-item>
+              <el-form-item label="审核状态">
+                <span class="view-only-value audit-status" :class="'audit-status--' + detailExtra.auditStatus">{{
+                  detailExtra.auditStatusLabel || "--"
+                }}</span>
+              </el-form-item>
+              <el-form-item label="审核时间">
+                <span class="view-only-value">{{ detailExtra.auditTime || "--" }}</span>
+              </el-form-item>
+              <el-form-item v-if="detailExtra.auditStatus === 2" label="拒绝原因">
+                <div class="view-only-value reject-reason-text">
+                  {{ detailExtra.auditReason || "--" }}
+                </div>
+              </el-form-item>
+              <el-form-item label="在线状态">
+                <span class="view-only-value">{{ detailExtra.onlineStatusLabel || "--" }}</span>
+              </el-form-item>
+            </template>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+
+    <div v-if="!viewMode" class="page-footer">
+      <div class="footer-inner">
+        <el-button @click="goBack"> 取消 </el-button>
+        <el-button type="primary" :loading="submitting" @click="submit"> 提交 </el-button>
+      </div>
+    </div>
+
+    <GuestSelectDialog v-model="guestSelectVisible" :selected-ids="selectedGuestIds" @confirm="onGuestsConfirm" />
+  </div>
+</template>
+
+<script setup lang="ts" name="performanceEdit">
+import { ref, computed, watch, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { User, Close, ArrowLeft } from "@element-plus/icons-vue";
+import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
+import UploadImgs from "@/components/Upload/Imgs.vue";
+import { uploadImgStore } from "@/api/modules/upload";
+import {
+  getPerformanceDetail,
+  saveOrUpdatePerformance,
+  detailToFormModel,
+  getDefaultFormModel,
+  buildPerformanceBackendPayload,
+  AUDIT_STATUS_LABEL
+} from "@/api/modules/performance";
+import GuestSelectDialog from "./components/GuestSelectDialog.vue";
+import type { GuestItem } from "./components/GuestSelectDialog.vue";
+
+const styleOptions = [
+  "民谣",
+  "爵士",
+  "摇滚",
+  "电音",
+  "流行",
+  "蓝调",
+  "AXAPPELLA (全人声伴奏)",
+  "SOUL",
+  "朋克",
+  "R&B",
+  "EDM",
+  "原创",
+  "古典",
+  "金属",
+  "说唱"
+];
+const weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
+
+const route = useRoute();
+const router = useRouter();
+const formRef = ref<FormInstance>();
+const id = ref<string>("");
+const viewMode = computed(() => route.query.mode === "detail");
+const submitting = ref(false);
+const posterFileList = ref<UploadUserFile[]>([]);
+const detailImageFileList = ref<UploadUserFile[]>([]);
+const guestSelectVisible = ref(false);
+
+const form = ref<Record<string, any>>(getDefaultFormModel());
+const detailExtra = ref<{
+  submitTime: string;
+  auditStatus: number;
+  auditStatusLabel: string;
+  auditTime: string;
+  auditReason: string;
+  onlineStatusLabel: string;
+}>({
+  submitTime: "",
+  auditStatus: 0,
+  auditStatusLabel: "",
+  auditTime: "",
+  auditReason: "",
+  onlineStatusLabel: ""
+});
+
+watch(
+  () => form.value.performanceType,
+  type => {
+    if (type === "特邀演出") form.value.frequency = "单次";
+  }
+);
+
+const singleStartDate = computed({
+  get: () => form.value.performanceTime?.startDate ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.startDate = v;
+  }
+});
+const singleStartTime = computed({
+  get: () => form.value.performanceTime?.startTime ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.startTime = v;
+  }
+});
+const singleEndDate = computed({
+  get: () => form.value.performanceTime?.endDate ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.endDate = v;
+  }
+});
+const singleEndTime = computed({
+  get: () => form.value.performanceTime?.endTime ?? "",
+  set: v => {
+    if (!form.value.performanceTime) form.value.performanceTime = {};
+    form.value.performanceTime.endTime = v;
+  }
+});
+const dailyDateStart = computed({
+  get: () => form.value.dailyPerformanceDateRange?.start ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceDateRange) form.value.dailyPerformanceDateRange = {};
+    form.value.dailyPerformanceDateRange.start = v;
+  }
+});
+const dailyDateEnd = computed({
+  get: () => form.value.dailyPerformanceDateRange?.end ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceDateRange) form.value.dailyPerformanceDateRange = {};
+    form.value.dailyPerformanceDateRange.end = v;
+  }
+});
+const dailyTimeStart = computed({
+  get: () => form.value.dailyPerformanceTimeRange?.start ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceTimeRange) form.value.dailyPerformanceTimeRange = {};
+    form.value.dailyPerformanceTimeRange.start = v;
+  }
+});
+const dailyTimeEnd = computed({
+  get: () => form.value.dailyPerformanceTimeRange?.end ?? "",
+  set: v => {
+    if (!form.value.dailyPerformanceTimeRange) form.value.dailyPerformanceTimeRange = {};
+    form.value.dailyPerformanceTimeRange.end = v;
+  }
+});
+const weeklyDateStart = computed({
+  get: () => form.value.weeklyPerformanceDateRange?.start ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceDateRange) form.value.weeklyPerformanceDateRange = {};
+    form.value.weeklyPerformanceDateRange.start = v;
+  }
+});
+const weeklyDateEnd = computed({
+  get: () => form.value.weeklyPerformanceDateRange?.end ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceDateRange) form.value.weeklyPerformanceDateRange = {};
+    form.value.weeklyPerformanceDateRange.end = v;
+  }
+});
+const weeklyTimeStart = computed({
+  get: () => form.value.weeklyPerformanceTimeRange?.start ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceTimeRange) form.value.weeklyPerformanceTimeRange = {};
+    form.value.weeklyPerformanceTimeRange.start = v;
+  }
+});
+const weeklyTimeEnd = computed({
+  get: () => form.value.weeklyPerformanceTimeRange?.end ?? "",
+  set: v => {
+    if (!form.value.weeklyPerformanceTimeRange) form.value.weeklyPerformanceTimeRange = {};
+    form.value.weeklyPerformanceTimeRange.end = v;
+  }
+});
+
+const selectedGuestIds = computed(() => form.value.guests?.map((g: any) => g.id) ?? []);
+
+const formRules: FormRules = {
+  name: [{ required: true, message: "请输入演出名称", trigger: "blur" }]
+};
+
+onMounted(() => {
+  id.value = (route.query.id as string) || "";
+  if (id.value) loadDetail();
+  else Object.assign(form.value, getDefaultFormModel());
+  syncPosterAndDetailFiles();
+});
+
+async function loadDetail() {
+  if (!id.value) return;
+  try {
+    const res: any = await getPerformanceDetail(id.value);
+    const raw = res?.data ?? res;
+    if (raw) {
+      Object.assign(form.value, detailToFormModel(raw));
+      detailExtra.value = {
+        submitTime: raw.createdTime ?? raw.submitTime ?? raw.createTime ?? "",
+        auditStatus: raw.reviewStatus ?? raw.auditStatus ?? 0,
+        auditStatusLabel: AUDIT_STATUS_LABEL[raw.reviewStatus ?? raw.auditStatus] ?? "",
+        auditTime: raw.auditTime ?? raw.reviewTime ?? "",
+        auditReason: raw.rejectReason ?? raw.auditReason ?? "",
+        onlineStatusLabel: (raw.onlineStatus ?? raw.shelfStatus) === 1 ? "上线" : "下线"
+      };
+    }
+    syncPosterAndDetailFiles();
+  } catch (e) {
+    ElMessage.error("获取详情失败");
+  }
+}
+
+function syncPosterAndDetailFiles() {
+  const poster = form.value.posterUrls?.[0] ?? form.value.imgUrl;
+  posterFileList.value = poster ? ([{ name: "poster", url: poster }] as UploadUserFile[]) : [];
+  const urls = form.value.detailPicUrl ?? [];
+  detailImageFileList.value = urls.map((u: string, i: number) => ({ name: `detail-${i}`, url: u })) as UploadUserFile[];
+}
+
+function onPosterChange(list: UploadUserFile[]) {
+  const url = list.length ? ((list[0].url as string) ?? "") : "";
+  form.value.posterUrls = url ? [url] : [];
+  form.value.imgUrl = url;
+}
+
+function onDetailImagesChange(list: UploadUserFile[]) {
+  form.value.detailPicUrl = list.map(f => (typeof f.url === "string" ? f.url : "")).filter(Boolean);
+}
+
+function openGuestSelect() {
+  guestSelectVisible.value = true;
+}
+
+function onGuestsConfirm(list: GuestItem[]) {
+  form.value.guests = list;
+}
+
+function removeGuest(g: any) {
+  form.value.guests = (form.value.guests || []).filter((x: any) => x.id !== g.id);
+}
+
+function goBack() {
+  router.back();
+}
+
+function goEdit() {
+  router.replace({ path: "/performance/edit", query: { id: id.value } });
+}
+
+function validateForm(): boolean {
+  if (!form.value.name?.trim()) {
+    ElMessage.warning("请输入演出名称");
+    return false;
+  }
+  const poster = form.value.posterUrls?.[0] ?? form.value.imgUrl;
+  if (!poster) {
+    ElMessage.warning("请上传演出海报");
+    return false;
+  }
+  if (!form.value.style?.length) {
+    ElMessage.warning("请选择演出风格");
+    return false;
+  }
+  if (form.value.frequency === "单次") {
+    const pt = form.value.performanceTime || {};
+    if (!pt.startDate || !pt.startTime || !pt.endDate || !pt.endTime) {
+      ElMessage.warning("请填写演出时间");
+      return false;
+    }
+  }
+  if (form.value.frequency === "每天定时进行") {
+    const dr = form.value.dailyPerformanceDateRange || {};
+    const tr = form.value.dailyPerformanceTimeRange || {};
+    if (!dr.start || !dr.end || !tr.start || !tr.end) {
+      ElMessage.warning("请填写演出日期与时间");
+      return false;
+    }
+  }
+  if (form.value.frequency === "每周定时进行") {
+    const wr = form.value.weeklyPerformanceDateRange || {};
+    const tr = form.value.weeklyPerformanceTimeRange || {};
+    if (!wr.start || !wr.end || !tr.start || !tr.end) {
+      ElMessage.warning("请填写演出日期与时间");
+      return false;
+    }
+  }
+  return true;
+}
+
+async function submit() {
+  if (!validateForm() || submitting.value) return;
+  submitting.value = true;
+  try {
+    const payload = buildPerformanceBackendPayload(form.value);
+    if (id.value) payload.id = id.value;
+    const res: any = await saveOrUpdatePerformance(payload);
+    if (res?.code === 200) {
+      ElMessage.success(id.value ? "编辑成功" : "新建成功");
+      router.back();
+    } else {
+      ElMessage.error(res?.msg || "提交失败");
+    }
+  } catch (e) {
+    ElMessage.error("提交失败");
+  } finally {
+    submitting.value = false;
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.performance-edit-page {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background: #f5f7fa;
+}
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 56px;
+  padding: 0 24px;
+  background: #ffffff;
+  border-bottom: 1px solid #ebeef5;
+  .header-left {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    font-size: 14px;
+    color: #606266;
+    cursor: pointer;
+    .back-icon {
+      font-size: 18px;
+    }
+  }
+  .page-title {
+    position: absolute;
+    left: 50%;
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #303133;
+    transform: translateX(-50%);
+  }
+  .header-right {
+    color: #606266;
+    cursor: pointer;
+    .close-icon {
+      font-size: 20px;
+    }
+  }
+}
+.page-content {
+  flex: 1;
+  padding: 24px;
+  overflow: auto;
+}
+.performance-form {
+  padding: 24px;
+  background: #ffffff;
+  border-radius: 8px;
+  :deep(.el-form-item__label) {
+    font-size: 14px;
+    color: #606266;
+  }
+  .form-input {
+    max-width: 360px;
+  }
+  .form-textarea {
+    width: 100%;
+    max-width: 100%;
+  }
+}
+.upload-wrap {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  .poster-upload,
+  .detail-upload {
+    :deep(.el-upload-list--picture-card) {
+      --el-upload-list-picture-card-size: 100px;
+    }
+    :deep(.el-upload--picture-card) {
+      width: 100px;
+      height: 100px;
+    }
+  }
+  .upload-tip {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+.form-tip {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+}
+.date-time-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  .sep {
+    color: #909399;
+  }
+}
+.guest-list {
+  margin-top: 8px;
+  .guest-item {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    padding: 6px 0;
+    .guest-name {
+      flex: 1;
+      font-size: 14px;
+    }
+    .guest-remove {
+      color: #909399;
+      cursor: pointer;
+      &:hover {
+        color: #f56c6c;
+      }
+    }
+  }
+}
+.view-only-value {
+  font-size: 14px;
+  color: #606266;
+}
+.reject-reason-text {
+  min-height: 60px;
+  padding: 8px 0;
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+.audit-status {
+  font-weight: 500;
+  &.audit-status--0 {
+    color: #e6a23c;
+  }
+  &.audit-status--1 {
+    color: #67c23a;
+  }
+  &.audit-status--2 {
+    color: #f56c6c;
+  }
+}
+.page-footer {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 64px;
+  background: #ffffff;
+  border-top: 1px solid #ebeef5;
+}
+.footer-inner {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  max-width: 1100px;
+  padding: 0 24px;
+}
+</style>

+ 291 - 0
src/views/performance/index.vue

@@ -0,0 +1,291 @@
+<template>
+  <div class="table-box">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <template #tableHeader>
+        <div class="table-header">
+          <el-button type="primary" @click="handleAdd"> 新增 </el-button>
+        </div>
+      </template>
+      <template #name="{ row }">
+        <span>{{ row.name || "--" }}</span>
+      </template>
+      <template #performanceTypeLabel="{ row }">
+        <span>{{ row.performanceTypeLabel || "--" }}</span>
+      </template>
+      <template #frequencyLabel="{ row }">
+        <span>{{ row.frequencyLabel || "--" }}</span>
+      </template>
+      <template #shelfStatus="{ row }">
+        <span>{{ row.onlineStatusLabel }}</span>
+      </template>
+      <template #auditStatusLabel="{ row }">
+        <span>{{ row.auditStatusLabel }}</span>
+      </template>
+      <template #submitTime="{ row }">
+        <span>{{ row.submitTime || "--" }}</span>
+      </template>
+      <template #operation="scope">
+        <el-button link type="primary" @click="handleDetail(scope.row)"> 查看详情 </el-button>
+        <!-- 已通过 + 已上线:编辑、下线、删除 -->
+        <template v-if="scope.row.auditStatus === 1 && scope.row.shelfStatus === 1">
+          <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
+          <el-button link type="primary" @click="handleShelfToggle(scope.row, 0)"> 下线 </el-button>
+          <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
+        </template>
+        <!-- 已通过 + 已下线:编辑、上线、删除 -->
+        <template v-else-if="scope.row.auditStatus === 1 && scope.row.shelfStatus === 0">
+          <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
+          <el-button link type="primary" @click="handleShelfToggle(scope.row, 1)"> 上线 </el-button>
+          <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
+        </template>
+        <!-- 已驳回:编辑、删除、查看拒绝原因 -->
+        <template v-else-if="scope.row.auditStatus === 2">
+          <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
+          <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
+          <el-button v-if="scope.row.auditReason" link type="primary" @click="handleViewReason(scope.row)">
+            查看拒绝原因
+          </el-button>
+        </template>
+      </template>
+    </ProTable>
+    <!-- 删除确认 -->
+    <el-dialog v-model="deleteDialogVisible" title="提示" width="500px">
+      <div style="padding: 20px 0">确定要删除该演出吗?</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deleteDialogVisible = false"> 取消 </el-button>
+          <el-button type="primary" @click="confirmDelete"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!-- 查看拒绝原因 -->
+    <el-dialog v-model="reasonDialogVisible" title="查看拒绝原因" width="600px">
+      <div class="reject-reason-content">
+        <div class="reject-reason-item">
+          <div class="reject-reason-label">演出名称:</div>
+          <div class="reject-reason-value">
+            {{ currentReasonData.name || "--" }}
+          </div>
+        </div>
+        <div class="reject-reason-item">
+          <div class="reject-reason-label">拒绝原因:</div>
+          <div class="reject-reason-value reject-reason-text">
+            {{ currentReasonData.reason || "暂无拒绝原因" }}
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="reasonDialogVisible = false"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="performance">
+import { reactive, ref } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import { localGet } from "@/utils";
+import {
+  getPerformancePage,
+  transformPerformanceListResponse,
+  deletePerformance,
+  setPerformanceOnlineStatus,
+  type PerformanceRow
+} from "@/api/modules/performance";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+const deleteDialogVisible = ref(false);
+const deletingItem = ref<PerformanceRow | null>(null);
+const reasonDialogVisible = ref(false);
+const currentReasonData = ref<{ name: string; reason: string }>({ name: "", reason: "" });
+
+const initParam = reactive({
+  storeId: localGet("createdId") || ""
+});
+
+const columns: ColumnProps<PerformanceRow>[] = [
+  { type: "index", label: "序号", width: 60 },
+  {
+    prop: "name",
+    label: "名称",
+    search: { el: "input", props: { placeholder: "请输入" } }
+  },
+  {
+    prop: "performanceType",
+    label: "类型",
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: [
+      { label: "特邀演出", value: 0 },
+      { label: "常规演出", value: 1 }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  },
+  { prop: "frequencyLabel", label: "频次" },
+  {
+    prop: "shelfStatus",
+    label: "在线状态",
+    search: { el: "select", props: { placeholder: "请选择" } },
+    enum: [
+      { label: "上线", value: 1 },
+      { label: "下线", value: 0 }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "auditStatus",
+    label: "审核状态",
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: [
+      { label: "待审核", value: 0 },
+      { label: "已通过", value: 1 },
+      { label: "已驳回", value: 2 }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "submitTime",
+    label: "提交时间"
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 280 }
+];
+
+// 搜索区增加「在线状态」「提交时间」:在线状态用现有 enum 列即可;提交时间用自定义 search 或两字段
+// 若后端支持提交时间范围,可在 initParam 或列上增加 search
+const getTableList = (params: any) => {
+  const query: any = {
+    storeId: initParam.storeId,
+    pageNum: params.pageNum ?? 1,
+    pageSize: params.pageSize ?? 10,
+    performanceName: params.name ?? undefined,
+    reviewStatus: params.auditStatus,
+    performanceType: params.performanceType,
+    onlineStatus: params.shelfStatus
+  };
+  return getPerformancePage(query);
+};
+
+const dataCallback = (payload: any) => {
+  return transformPerformanceListResponse(payload);
+};
+
+const handleAdd = () => {
+  router.push({ path: "/performance/edit" });
+};
+
+const handleEdit = (row: PerformanceRow) => {
+  router.push({ path: "/performance/edit", query: { id: String(row.id) } });
+};
+
+const handleDetail = (row: PerformanceRow) => {
+  router.push({ path: "/performance/edit", query: { id: String(row.id), mode: "detail" } });
+};
+
+const handleDelete = (row: PerformanceRow) => {
+  deletingItem.value = row;
+  deleteDialogVisible.value = true;
+};
+
+const confirmDelete = async () => {
+  if (!deletingItem.value) return;
+  try {
+    const res: any = await deletePerformance(deletingItem.value.id);
+    if (res?.code === 200) {
+      ElMessage.success("删除成功");
+      deleteDialogVisible.value = false;
+      deletingItem.value = null;
+      proTable.value?.getTableList();
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
+    }
+  } catch (error) {
+    console.error("删除失败:", error);
+    ElMessage.error("删除失败");
+  }
+};
+
+const handleShelfToggle = async (row: PerformanceRow, onlineStatus: number) => {
+  const actionText = onlineStatus === 1 ? "上线" : "下线";
+  try {
+    await ElMessageBox.confirm(`确定要${actionText}该演出吗?`, "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+  } catch {
+    return;
+  }
+  try {
+    const res: any = await setPerformanceOnlineStatus(row.id, onlineStatus);
+    if (res?.code === 200) {
+      ElMessage.success(`${actionText}成功`);
+      proTable.value?.getTableList();
+    } else {
+      ElMessage.error(res?.msg || "操作失败");
+    }
+  } catch (error) {
+    console.error("操作失败:", error);
+    ElMessage.error("操作失败");
+  }
+};
+
+const handleViewReason = (row: PerformanceRow) => {
+  currentReasonData.value = {
+    name: row.name || "",
+    reason: row.auditReason || "暂无原因"
+  };
+  reasonDialogVisible.value = true;
+};
+</script>
+
+<style scoped lang="scss">
+.table-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  padding: 12px 0;
+}
+.reject-reason-content {
+  padding: 20px 0;
+  .reject-reason-item {
+    display: flex;
+    margin-bottom: 20px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .reject-reason-label {
+      flex-shrink: 0;
+      min-width: 100px;
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .reject-reason-value {
+      flex: 1;
+      font-size: 14px;
+      color: #303133;
+      word-break: break-word;
+      &.reject-reason-text {
+        min-height: 80px;
+        padding: 12px;
+        line-height: 1.6;
+        white-space: pre-wrap;
+        background-color: #f5f7fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 28 - 0
src/views/priceList/index.vue

@@ -144,6 +144,7 @@ const baseColumns: ColumnProps<PriceListRow>[] = [
     prop: "dishName",
     label: "名称",
     search: {
+      key: "prodName",
       el: "input",
       props: {
         placeholder: "请输入名称"
@@ -167,6 +168,7 @@ const baseColumns: ColumnProps<PriceListRow>[] = [
     prop: "auditStatus",
     label: "审核状态",
     search: {
+      key: "status",
       el: "select",
       props: { placeholder: "请选择审核状态" }
     },
@@ -190,6 +192,32 @@ const baseColumns: ColumnProps<PriceListRow>[] = [
     ],
     fieldNames: { label: "label", value: "value" }
   },
+  {
+    prop: "startCreatedTime",
+    label: "提交时间",
+    isShow: false,
+    search: {
+      el: "date-picker",
+      props: {
+        type: "datetime",
+        valueFormat: "YYYY-MM-DD HH:mm:ss",
+        placeholder: "请输入"
+      }
+    }
+  },
+  {
+    prop: "endCreatedTime",
+    label: "至",
+    isShow: false,
+    search: {
+      el: "date-picker",
+      props: {
+        type: "datetime",
+        valueFormat: "YYYY-MM-DD HH:mm:ss",
+        placeholder: "请输入"
+      }
+    }
+  },
   { prop: "operation", label: "操作", fixed: "right", width: 280 }
 ];