From b58f53769f004fb0ba6b5aedf55ef602850c7c97 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期日, 20 九月 2026 15:46:05 +0800
Subject: [PATCH] 天津豹鸣app 1.部署修改

---
 src/pages/projectManagement/project/edit.vue |  574 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 574 insertions(+), 0 deletions(-)

diff --git a/src/pages/projectManagement/project/edit.vue b/src/pages/projectManagement/project/edit.vue
new file mode 100644
index 0000000..54ce91e
--- /dev/null
+++ b/src/pages/projectManagement/project/edit.vue
@@ -0,0 +1,574 @@
+<!--
+  椤圭洰鏂板 / 缂栬緫
+
+  琛ㄥ崟鍒嗗尯瀵归綈绠$悊绔� formDia.vue锛氬熀纭�璧勬枡 / 浜у搧淇℃伅 / 椤圭洰鍥㈤槦 / 鏀惰揣鍦板潃 / 鑱旂郴淇℃伅 / 闄勪欢銆�
+  绠$悊绔繕鏈変竴涓�岄」鐩樁娈点�嶅瓙琛紝鏁村潡鏄敞閲婃帀鐨勪笖涓嶅湪 /save 鐨勫叆鍙傞噷锛�
+  鎵�浠ヨ繖閲屼篃涓嶅仛鍙紪杈戦樁娈佃〃锛堝仛浜嗕細闈欓粯涓㈡暟鎹級锛涢樁娈甸�氳繃璇︽儏椤垫煡鐪嬨�侀�氳繃銆岃繘搴︽眹鎶ャ�嶅啓鍏ャ��
+
+  鍩虹璧勬枡閲屻�岄璁″伐鏈熴�嶅湪绠$悊绔彲濉絾涓嶅湪鎻愪氦缁撴瀯閲岋紙鍚庣鎸夎鍒掕捣姝㈡棩鏈熸淳鐢燂級锛�
+  鎵�浠ヨ繖閲屽仛鎴愬彧璇绘淳鐢熷�硷紝閬垮厤鐢ㄦ埛濉簡涓嶇敓鏁堛��
+-->
+<template>
+  <view class="project-edit">
+    <PageHeader :title="pageTitle"
+                @back="goBack" />
+
+    <view class="form-container">
+      <up-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="120"
+               input-align="right"
+               error-message-align="right">
+        <u-cell-group title="鍩虹璧勬枡"
+                      class="form-section">
+          <up-form-item v-if="projectId"
+                        label="鍗曟嵁缂栧彿">
+            <up-input :model-value="form.billNo || '绯荤粺鐢熸垚'"
+                      disabled />
+          </up-form-item>
+          <up-form-item label="椤圭洰鍚嶇О"
+                        prop="projectName"
+                        required>
+            <up-input v-model="form.projectName"
+                      placeholder="璇疯緭鍏ラ」鐩悕绉�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="瀹㈡埛鍚嶇О"
+                        prop="customerName">
+            <up-input v-model="form.customerName"
+                      placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="绔嬮」鏃ユ湡"
+                        prop="setupDate">
+            <FormPicker v-model="form.setupDate"
+                        type="date"
+                        placeholder="璇烽�夋嫨绔嬮」鏃ユ湡" />
+          </up-form-item>
+          <up-form-item label="椤圭洰鏉ユ簮"
+                        prop="projectSource">
+            <up-input v-model="form.projectSource"
+                      placeholder="璇疯緭鍏ラ」鐩潵婧�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="绔嬮」浜�"
+                        prop="creatorName">
+            <up-input v-model="form.creatorName"
+                      placeholder="璇疯緭鍏ョ珛椤逛汉"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="椤圭洰绫诲瀷"
+                        prop="projectManagementPlanId"
+                        required>
+            <FormPicker v-model="form.projectManagementPlanId"
+                        type="select"
+                        title="閫夋嫨椤圭洰绫诲瀷"
+                        :options="projectTypeOptions"
+                        placeholder="璇烽�夋嫨椤圭洰绫诲瀷" />
+          </up-form-item>
+          <up-form-item label="璁″垝寮�濮嬫棩鏈�"
+                        prop="planStartDate">
+            <FormPicker v-model="form.planStartDate"
+                        type="date"
+                        placeholder="璇烽�夋嫨璁″垝寮�濮嬫棩鏈�" />
+          </up-form-item>
+          <up-form-item label="璁″垝瀹屾垚鏃ユ湡"
+                        prop="planEndDate">
+            <FormPicker v-model="form.planEndDate"
+                        type="date"
+                        placeholder="璇烽�夋嫨璁″垝瀹屾垚鏃ユ湡" />
+          </up-form-item>
+          <up-form-item label="棰勮宸ユ湡(澶�)">
+            <up-input :model-value="estimatedDays === null ? '鎸夎鍒掓棩鏈熻嚜鍔ㄨ绠�' : String(estimatedDays)"
+                      disabled />
+          </up-form-item>
+          <up-form-item label="椤圭洰閲戦"
+                        prop="projectAmount">
+            <up-input v-model="form.projectAmount"
+                      type="number"
+                      placeholder="璇疯緭鍏ラ」鐩噾棰�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="瀹℃牳鐘舵��"
+                        prop="auditStatus">
+            <FormPicker v-model="form.auditStatus"
+                        type="select"
+                        title="閫夋嫨瀹℃牳鐘舵��"
+                        :options="auditOptions"
+                        placeholder="璇烽�夋嫨瀹℃牳鐘舵��" />
+          </up-form-item>
+          <up-form-item label="澶囨敞"
+                        prop="remark">
+            <up-textarea v-model="form.remark"
+                         placeholder="璇疯緭鍏ュ娉�"
+                         :maxlength="100"
+                         count
+                         auto-height />
+          </up-form-item>
+        </u-cell-group>
+
+        <u-cell-group title="浜у搧淇℃伅"
+                      class="form-section">
+          <view class="section-tools">
+            <up-button type="primary"
+                       size="small"
+                       text="娣诲姞浜у搧"
+                       @click="addProduct" />
+          </view>
+          <view v-if="productList.length === 0"
+                class="empty-text">
+            <text>璇疯嚦灏戞坊鍔犱竴鏉′骇鍝佷俊鎭�</text>
+          </view>
+          <view v-else
+                class="row-list">
+            <ProductRow v-for="(row, index) in productList"
+                        :key="row.uid"
+                        :row="row"
+                        :index="index"
+                        :tax-rate-options="taxRateOptions"
+                        @remove="removeProduct(index)" />
+          </view>
+        </u-cell-group>
+
+        <u-cell-group title="椤圭洰鍥㈤槦"
+                      class="form-section">
+          <view class="section-tools">
+            <up-button type="primary"
+                       size="small"
+                       text="娣诲姞鎴愬憳"
+                       @click="addTeamRow" />
+          </view>
+          <view v-if="teamList.length === 0"
+                class="empty-text">
+            <text>鏆傛棤椤圭洰鎴愬憳</text>
+          </view>
+          <view v-else
+                class="row-list">
+            <TeamRow v-for="(row, index) in teamList"
+                     :key="index"
+                     :row="row"
+                     :index="index"
+                     :user-options="userOptions"
+                     :role-options="roleOptions"
+                     @remove="removeTeamRow(index)" />
+          </view>
+        </u-cell-group>
+
+        <u-cell-group title="鏀惰揣鍦板潃"
+                      class="form-section">
+          <view v-if="addressList.length === 0"
+                class="section-tools">
+            <up-button type="primary"
+                       size="small"
+                       text="娣诲姞鍦板潃"
+                       @click="addAddressRow" />
+          </view>
+          <view v-else
+                class="row-list">
+            <AddressRow v-for="(row, index) in addressList"
+                        :key="index"
+                        :row="row" />
+          </view>
+        </u-cell-group>
+
+        <u-cell-group title="鑱旂郴淇℃伅"
+                      class="form-section">
+          <up-form-item label="鑱旂郴浜哄鍚�"
+                        prop="contactName">
+            <up-input v-model="form.contactName"
+                      placeholder="璇疯緭鍏ヨ仈绯讳汉濮撳悕"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="鎬у埆"
+                        prop="contactGender">
+            <FormPicker v-model="form.contactGender"
+                        type="select"
+                        title="閫夋嫨鎬у埆"
+                        :options="GENDER_OPTIONS"
+                        placeholder="璇烽�夋嫨鎬у埆" />
+          </up-form-item>
+          <up-form-item label="鐢熸棩"
+                        prop="contactBirthday">
+            <FormPicker v-model="form.contactBirthday"
+                        type="date"
+                        placeholder="璇烽�夋嫨鐢熸棩" />
+          </up-form-item>
+          <up-form-item label="閮ㄩ棬"
+                        prop="contactDept">
+            <up-input v-model="form.contactDept"
+                      placeholder="璇疯緭鍏ラ儴闂�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="鑱屽姟"
+                        prop="contactJob">
+            <up-input v-model="form.contactJob"
+                      placeholder="璇疯緭鍏ヨ亴鍔�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="鎵嬫満鍙风爜"
+                        prop="contactMobile">
+            <up-input v-model="form.contactMobile"
+                      type="number"
+                      placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="閭"
+                        prop="contactEmail">
+            <up-input v-model="form.contactEmail"
+                      placeholder="璇疯緭鍏ラ偖绠�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="QQ"
+                        prop="contactQq">
+            <up-input v-model="form.contactQq"
+                      placeholder="璇疯緭鍏� QQ"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="寰俊鍙风爜"
+                        prop="contactWechat">
+            <up-input v-model="form.contactWechat"
+                      placeholder="璇疯緭鍏ュ井淇″彿鐮�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="浼佷笟寰俊"
+                        prop="contactWorkWechat">
+            <up-input v-model="form.contactWorkWechat"
+                      placeholder="璇疯緭鍏ヤ紒涓氬井淇�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="鍦板潃"
+                        prop="contactAddress">
+            <up-input v-model="form.contactAddress"
+                      placeholder="璇疯緭鍏ュ湴鍧�"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="鑱旂郴淇℃伅澶囨敞"
+                        prop="contactRemark">
+            <up-textarea v-model="form.contactRemark"
+                         placeholder="璇疯緭鍏ュ娉�"
+                         :maxlength="200"
+                         count
+                         auto-height />
+          </up-form-item>
+        </u-cell-group>
+
+        <u-cell-group title="闄勪欢"
+                      class="form-section">
+          <view class="upload-block">
+            <CommonUpload v-model="attachmentFiles"
+                          accept="all" />
+          </view>
+        </u-cell-group>
+      </up-form>
+    </view>
+
+    <FooterButtons :loading="loading"
+                   confirmText="淇濆瓨"
+                   @cancel="goBack"
+                   @confirm="handleSubmit" />
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref } from "vue";
+  import { onLoad } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import FormPicker from "@/components/FormPicker.vue";
+  import CommonUpload from "@/components/CommonUpload.vue";
+  import ProductRow from "./_components/ProductRow.vue";
+  import TeamRow from "./_components/TeamRow.vue";
+  import AddressRow from "./_components/AddressRow.vue";
+  import { useDict } from "@/utils/dict";
+  import { parseDate } from "@/utils/warning";
+  import { unwrapList } from "@/utils/list";
+  import { userListNoPageByTenantId } from "@/api/system/user";
+  import { findRoleListPage } from "@/api/projectManagement/role";
+  import { listPlan } from "@/api/projectManagement/projectType";
+  import { getProject, saveProject } from "@/api/projectManagement/project";
+  import {
+    GENDER_OPTIONS,
+    createEmptyProduct,
+    toForm,
+    toPayload,
+  } from "./_utils/mapper";
+  import { toPickerOptions } from "./_utils/dict";
+
+  const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+  const { project_management, tax_rate } = useDict(
+    "project_management",
+    "tax_rate"
+  );
+  const auditOptions = computed(() => toPickerOptions(project_management.value));
+  const taxRateOptions = computed(() =>
+    (tax_rate.value || []).map(item => ({ label: item.label, value: item.value }))
+  );
+
+  const formRef = ref();
+  const loading = ref(false);
+  const projectId = ref("");
+  const projectTypeOptions = ref([]);
+  const userOptions = ref([]);
+  const roleOptions = ref([]);
+
+  const form = ref({
+    id: undefined,
+    billNo: "",
+    projectName: "",
+    clientId: "",
+    customerName: "",
+    parentProjectId: "",
+    parentProjectName: "",
+    projectManagementPlanId: "",
+    projectSource: "",
+    managerId: "",
+    creatorName: "",
+    salesmanId: "",
+    salesmanName: "",
+    departmentId: "",
+    departmentName: "",
+    setupDate: "",
+    planStartDate: "",
+    planEndDate: "",
+    actualStartDate: "",
+    actualEndDate: "",
+    orderDate: "",
+    projectAmount: "",
+    billStatus: "",
+    auditStatus: "",
+    projectStage: "",
+    remark: "",
+    contactName: "",
+    contactGender: "",
+    contactBirthday: "",
+    contactDept: "",
+    contactJob: "",
+    contactMobile: "",
+    contactEmail: "",
+    contactQq: "",
+    contactWechat: "",
+    contactWorkWechat: "",
+    contactAddress: "",
+    contactRemark: "",
+  });
+
+  const teamList = ref([]);
+  const addressList = ref([]);
+  const productList = ref([]);
+  const attachmentFiles = ref([]);
+
+  const rules = {
+    projectName: [{ required: true, message: "璇疯緭鍏ラ」鐩悕绉�", trigger: "blur" }],
+    projectManagementPlanId: [
+      { required: true, message: "璇烽�夋嫨椤圭洰绫诲瀷", trigger: "change" },
+    ],
+  };
+
+  const pageTitle = computed(() => (projectId.value ? "缂栬緫椤圭洰" : "鏂板椤圭洰"));
+
+  // 鍚庣鎸夎鍒掕捣姝㈡棩鏈熸淳鐢� estimatedDays锛岃繖閲屽彧鍋氬睍绀猴紝涓嶅弬涓庢彁浜�
+  const estimatedDays = computed(() => {
+    const start = parseDate(form.value.planStartDate);
+    const end = parseDate(form.value.planEndDate);
+    if (!start || !end || end < start) return null;
+    return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
+  });
+
+  const goBack = () => uni.navigateBack();
+
+  const addProduct = () => productList.value.push(createEmptyProduct());
+
+  const removeProduct = index => {
+    productList.value.splice(index, 1);
+  };
+
+  const addTeamRow = () =>
+    teamList.value.push({
+      memberId: "",
+      memberName: "",
+      roleId: "",
+      roleName: "",
+      enterDate: "",
+      leaveDate: "",
+      phone: "",
+      remark: "",
+    });
+
+  const removeTeamRow = index => {
+    teamList.value.splice(index, 1);
+  };
+
+  const addAddressRow = () =>
+    addressList.value.push({ receiver: "", phone: "", address: "" });
+
+  const loadProjectTypes = () => {
+    listPlan({ current: 1, size: 999 })
+      .then(res => {
+        projectTypeOptions.value = unwrapList(res).map(item => ({
+          name: item.name || item.planName || "",
+          value: item.id,
+        }));
+      })
+      .catch(() => {
+        projectTypeOptions.value = [];
+      });
+  };
+
+  const loadUsers = () => {
+    userListNoPageByTenantId()
+      .then(res => {
+        const rows = unwrapList(res);
+        userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+          name: item.nickName || item.userName || "",
+          value: item.userId ?? item.id,
+        }));
+      })
+      .catch(() => {
+        userOptions.value = [];
+      });
+  };
+
+  const loadRoles = () => {
+    findRoleListPage({ pageNum: 1, pageSize: 999 })
+      .then(res => {
+        roleOptions.value = unwrapList(res).map(item => ({
+          name: item.roleName || item.name || "",
+          value: item.id,
+        }));
+      })
+      .catch(() => {
+        roleOptions.value = [];
+      });
+  };
+
+  const loadDetail = async () => {
+    try {
+      const res = await getProject(projectId.value);
+      const detail = res?.data?.data ?? res?.data ?? res;
+      const mapped = toForm(detail || {});
+      form.value = { ...form.value, ...mapped.form };
+      teamList.value = mapped.teamList;
+      addressList.value = mapped.addressList;
+      productList.value = mapped.productList;
+      attachmentFiles.value = mapped.attachments;
+    } catch (error) {
+      uni.showToast({ title: "鑾峰彇椤圭洰璇︽儏澶辫触", icon: "none" });
+    }
+  };
+
+  const validateProducts = () => {
+    if (productList.value.length === 0) {
+      uni.showToast({ title: "璇锋坊鍔犱骇鍝佷俊鎭�", icon: "none" });
+      return false;
+    }
+    const invalid = productList.value.some(row => !row.productCategoryId);
+    if (invalid) {
+      uni.showToast({ title: "璇蜂负姣忔潯浜у搧閫夋嫨浜у搧澶х被", icon: "none" });
+      return false;
+    }
+    return true;
+  };
+
+  const handleSubmit = async () => {
+    const valid = await formRef.value.validate().catch(() => false);
+    if (!valid) return;
+    if (!validateProducts()) return;
+
+    // CommonUpload 瀛樼殑鏄笂浼犳帴鍙h繑鍥炵殑瀵硅薄锛屾彁浜ゅ彧瑕� id
+    const attachmentIds = attachmentFiles.value
+      .map(item => item?.id ?? item?.attachmentId)
+      .filter(Boolean);
+
+    loading.value = true;
+    saveProject(
+      toPayload({
+        form: form.value,
+        teamList: teamList.value,
+        addressList: addressList.value,
+        productList: productList.value,
+        attachmentIds,
+      })
+    )
+      .then(() => {
+        uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+        setTimeout(() => uni.navigateBack(), 300);
+      })
+      .catch(() => {
+        uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  onLoad(async options => {
+    projectId.value = options?.id || "";
+    if (projectId.value) form.value.id = projectId.value;
+
+    loadProjectTypes();
+    loadUsers();
+    loadRoles();
+
+    if (projectId.value) {
+      await loadDetail();
+    } else {
+      // 鏂板鏃跺厛閾轰竴鏉$┖琛岋紝鍑忓皯涓�娆$偣鍑伙紱浜у搧蹇呴』鑷冲皯涓�鏉★紝鍚庣浼氭牎楠�
+      addProduct();
+      addAddressRow();
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+
+  .project-edit {
+    min-height: 100vh;
+    background: #f8f9fa;
+    padding-bottom: 100px;
+  }
+
+  .form-container {
+    padding: 12px 12px 0;
+  }
+
+  .form-section {
+    margin-bottom: 12px;
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+  }
+
+  .section-tools {
+    display: flex;
+    justify-content: flex-end;
+    padding: 12px 12px 0;
+  }
+
+  .row-list {
+    padding: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .empty-text {
+    padding: 16px 12px;
+    color: #999;
+    font-size: 14px;
+  }
+
+  .upload-block {
+    padding: 12px;
+  }
+
+  :deep(.u-cell-group__title) {
+    padding: 14px 18px 10px !important;
+    font-size: 15px !important;
+    font-weight: 600 !important;
+    color: #22324d !important;
+    background: #f8fbff !important;
+  }
+</style>

--
Gitblit v1.9.3