addProject.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <template>
  2. <view class="app-container pb-24">
  3. <TopBar title="新增项目" show-back />
  4. <view class="px-4 py-4 gap-y-4">
  5. <!-- 基本信息 -->
  6. <view class="card p-4">
  7. <view class="detail-section-title mb-4">
  8. <view class="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center mr-3">
  9. <text class="uni-icons uniui-folder-add-filled text-primary"></text>
  10. </view>
  11. <text>基本信息</text>
  12. </view>
  13. <view class="gap-y-4">
  14. <view>
  15. <text class="form-label">
  16. 项目名称
  17. <text class="text-red-500">*</text>
  18. </text>
  19. <uni-easyinput class="form-input" placeholder="请输入项目名称" v-model="form.name" @blur="validateField('name')" />
  20. <text v-if="errors.name" class="text-red-500 text-xs mt-1">{{ errors.name }}</text>
  21. </view>
  22. <view>
  23. <text class="form-label">
  24. 项目类型
  25. <text class="text-red-500">*</text>
  26. </text>
  27. <picker mode="selector" :range="types" :value="typeIndex" @change="onTypeChange">
  28. <view class="form-select flex items-center" :class="{ 'text-gray-400': !form.type }">
  29. <text class="uni-icons uniui-arrowdown text-gray-400 mr-2"></text>
  30. {{ form.type || '请选择项目类型' }}
  31. </view>
  32. </picker>
  33. <text v-if="errors.type" class="text-red-500 text-xs mt-1">{{ errors.type }}</text>
  34. </view>
  35. <view class="grid grid-cols-3 gap-3">
  36. <view>
  37. <text class="form-label">省</text>
  38. <uni-easyinput class="form-input" placeholder="省" v-model="form.province" />
  39. </view>
  40. <view>
  41. <text class="form-label">市</text>
  42. <uni-easyinput class="form-input" placeholder="市" v-model="form.city" />
  43. </view>
  44. <view>
  45. <text class="form-label">区/县</text>
  46. <uni-easyinput class="form-input" placeholder="区/县" v-model="form.district" />
  47. </view>
  48. </view>
  49. <view>
  50. <text class="form-label">
  51. 详细地址
  52. <text class="text-red-500">*</text>
  53. </text>
  54. <uni-easyinput class="form-input" placeholder="请输入详细地址" v-model="form.address" @blur="validateField('address')" />
  55. <text v-if="errors.address" class="text-red-500 text-xs mt-1">{{ errors.address }}</text>
  56. </view>
  57. </view>
  58. </view>
  59. <!-- 客户信息 -->
  60. <view class="card p-4">
  61. <view class="detail-section-title mb-4">
  62. <view class="w-8 h-8 rounded-lg bg-purple-100 flex items-center justify-center mr-3">
  63. <text class="uni-icons uniui-person-filled text-purple-500"></text>
  64. </view>
  65. <text>客户信息</text>
  66. </view>
  67. <view class="gap-y-4">
  68. <view>
  69. <text class="form-label">
  70. 客户名称
  71. <text class="text-red-500">*</text>
  72. </text>
  73. <uni-easyinput class="form-input" placeholder="请输入客户名称" v-model="form.customerName" @blur="validateField('customerName')" />
  74. <text v-if="errors.customerName" class="text-red-500 text-xs mt-1">{{ errors.customerName }}</text>
  75. </view>
  76. <view>
  77. <text class="form-label">
  78. 联系电话
  79. <text class="text-red-500">*</text>
  80. </text>
  81. <input
  82. class="form-input"
  83. placeholder="请输入联系电话"
  84. v-model="form.customerPhone"
  85. type="number"
  86. maxlength="11"
  87. @blur="validateField('customerPhone')"
  88. />
  89. <text v-if="errors.customerPhone" class="text-red-500 text-xs mt-1">{{ errors.customerPhone }}</text>
  90. </view>
  91. </view>
  92. </view>
  93. <!-- 合同信息 -->
  94. <view class="card p-4">
  95. <view class="detail-section-title mb-4">
  96. <view class="w-8 h-8 rounded-lg bg-green-100 flex items-center justify-center mr-3">
  97. <text class="uni-icons uniui-wallet-filled text-green-500"></text>
  98. </view>
  99. <text>合同信息</text>
  100. </view>
  101. <view class="gap-y-4">
  102. <view>
  103. <text class="form-label">合同金额(元)</text>
  104. <uni-easyinput class="form-input" placeholder="请输入合同金额" v-model="form.contractAmount" type="digit" />
  105. </view>
  106. <view class="grid grid-cols-2 gap-3">
  107. <view>
  108. <text class="form-label">合同开始日期</text>
  109. <picker mode="date" :value="form.contractStartDate" @change="onStartDateChange">
  110. <view class="form-select flex items-center text-gray-700">
  111. <text class="uni-icons uniui-calendar text-gray-400 mr-2"></text>
  112. {{ form.contractStartDate || '选择日期' }}
  113. </view>
  114. </picker>
  115. </view>
  116. <view>
  117. <text class="form-label">合同结束日期</text>
  118. <picker mode="date" :value="form.contractEndDate" @change="onEndDateChange">
  119. <view class="form-select flex items-center text-gray-700">
  120. <text class="uni-icons uniui-calendar text-gray-400 mr-2"></text>
  121. {{ form.contractEndDate || '选择日期' }}
  122. </view>
  123. </picker>
  124. </view>
  125. </view>
  126. <view>
  127. <text class="form-label">服务周期</text>
  128. <uni-easyinput class="form-input" placeholder="如:每周2次" v-model="form.serviceCycle" />
  129. </view>
  130. </view>
  131. </view>
  132. <!-- 项目联系人 -->
  133. <view class="card p-4">
  134. <view class="detail-section-title mb-4">
  135. <view class="w-8 h-8 rounded-lg bg-orange-100 flex items-center justify-center mr-3">
  136. <text class="uni-icons uniui-personadd-filled text-orange-500"></text>
  137. </view>
  138. <text>项目联系人</text>
  139. </view>
  140. <view class="gap-y-4">
  141. <view
  142. v-for="(contact, index) in form.contacts"
  143. :key="index"
  144. class="border border-gray-200 rounded-xl p-4"
  145. >
  146. <view class="flex justify-between items-center mb-3">
  147. <text class="text-sm font-semibold text-gray-700">联系人 {{ index + 1 }}</text>
  148. <text class="text-red-500 text-sm flex items-center" @click="removeContact(index)">
  149. <text class="uni-icons uniui-trash text-sm mr-1"></text>
  150. 删除
  151. </text>
  152. </view>
  153. <view class="gap-y-3">
  154. <uni-easyinput class="form-input mb-2" placeholder="姓名" v-model="contact.name" />
  155. <uni-easyinput class="form-input mb-2" placeholder="角色/职位" v-model="contact.role" />
  156. <uni-easyinput class="form-input" placeholder="联系电话" v-model="contact.phone" type="number" maxlength="11" />
  157. </view>
  158. </view>
  159. <button
  160. class="w-full py-3 border border-dashed border-gray-300 rounded-xl text-sm text-gray-500 flex items-center justify-center bg-transparent"
  161. @click="addContact"
  162. >
  163. <text class="uni-icons uniui-plusempty mr-1"></text>
  164. 添加联系人
  165. </button>
  166. </view>
  167. </view>
  168. <!-- 服务点信息 -->
  169. <view class="card p-4">
  170. <view class="detail-section-title mb-4">
  171. <view class="w-8 h-8 rounded-lg bg-cyan-100 flex items-center justify-center mr-3">
  172. <text class="uni-icons uniui-location-filled text-cyan-500"></text>
  173. </view>
  174. <text>服务点信息</text>
  175. </view>
  176. <view class="gap-y-4">
  177. <view
  178. v-for="(point, index) in form.servicePoints"
  179. :key="index"
  180. class="border border-gray-200 rounded-xl p-4"
  181. >
  182. <view class="flex justify-between items-center mb-3">
  183. <text class="text-sm font-semibold text-gray-700">服务点 {{ index + 1 }}</text>
  184. <text class="text-red-500 text-sm flex items-center" @click="removeServicePoint(index)">
  185. <text class="uni-icons uniui-trash text-sm mr-1"></text>
  186. 删除
  187. </text>
  188. </view>
  189. <view class="gap-y-3">
  190. <uni-easyinput class="form-input mb-2" placeholder="服务点名称" v-model="point.name" />
  191. <uni-easyinput class="form-input mb-2" placeholder="服务点地址" v-model="point.address" />
  192. <uni-easyinput class="form-input mb-2" placeholder="服务类型" v-model="point.serviceType" />
  193. <uni-easyinput class="form-input mb-2" placeholder="联系人" v-model="point.contactName" />
  194. <uni-easyinput class="form-input" placeholder="联系电话" v-model="point.contactPhone" type="number" maxlength="11" />
  195. </view>
  196. </view>
  197. <button
  198. class="w-full py-3 border border-dashed border-gray-300 rounded-xl text-sm text-gray-500 flex items-center justify-center bg-transparent"
  199. @click="addServicePoint"
  200. >
  201. <text class="uni-icons uniui-plusempty mr-1"></text>
  202. 添加服务点
  203. </button>
  204. </view>
  205. </view>
  206. <!-- 备注 -->
  207. <view class="card p-4">
  208. <view class="detail-section-title mb-4">
  209. <view class="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center mr-3">
  210. <text class="uni-icons uniui-compose text-gray-500"></text>
  211. </view>
  212. <text>备注</text>
  213. </view>
  214. <textarea
  215. v-model="form.remark"
  216. class="w-full p-3 bg-surface rounded-xl text-sm text-gray-700"
  217. placeholder="请输入项目备注..."
  218. :maxlength="200"
  219. />
  220. </view>
  221. <!-- 提交按钮 -->
  222. <view class="px-4 pt-2 pb-6">
  223. <button
  224. class="w-full py-4 btn-primary rounded-2xl text-base font-semibold flex items-center justify-center"
  225. @click="submitForm"
  226. :disabled="submitting"
  227. >
  228. <text v-if="submitting" class="uni-icons uniui-spinner-cycle mr-2 animate-spin"></text>
  229. <text class="uni-icons uniui-checkmarkempty mr-2" v-else></text>
  230. {{ submitting ? '提交中...' : '提交项目' }}
  231. </button>
  232. </view>
  233. </view>
  234. </view>
  235. </template>
  236. <script setup lang="ts">
  237. import { ref, reactive } from 'vue'
  238. import TopBar from '../../components/common/TopBar.vue'
  239. import { createProject } from '../../api/project'
  240. const types = ['包年', '单次', '季度', '月付']
  241. const typeIndex = ref(0)
  242. const form = ref({
  243. name: '',
  244. type: '',
  245. province: '',
  246. city: '',
  247. district: '',
  248. address: '',
  249. customerName: '',
  250. customerPhone: '',
  251. contractAmount: '',
  252. contractStartDate: '',
  253. contractEndDate: '',
  254. serviceCycle: '',
  255. remark: '',
  256. contacts: [] as { name: string; role: string; phone: string }[],
  257. servicePoints: [] as { name: string; address: string; serviceType: string; contactName: string; contactPhone: string }[],
  258. })
  259. const errors = reactive<Record<string, string>>({
  260. name: '',
  261. type: '',
  262. address: '',
  263. customerName: '',
  264. customerPhone: '',
  265. })
  266. const submitting = ref(false)
  267. function onTypeChange(e: any) {
  268. typeIndex.value = e.detail.value
  269. form.value.type = types[typeIndex.value]
  270. errors.type = ''
  271. }
  272. function onStartDateChange(e: any) {
  273. form.value.contractStartDate = e.detail.value
  274. }
  275. function onEndDateChange(e: any) {
  276. form.value.contractEndDate = e.detail.value
  277. }
  278. function validateField(field: string) {
  279. const value = form.value[field as keyof typeof form.value] as string
  280. errors[field] = ''
  281. if (!value || !value.toString().trim()) {
  282. const fieldNames: Record<string, string> = {
  283. name: '项目名称',
  284. address: '详细地址',
  285. customerName: '客户名称',
  286. customerPhone: '联系电话',
  287. }
  288. errors[field] = `请输入${fieldNames[field] || field}`
  289. return false
  290. }
  291. if (field === 'customerPhone') {
  292. const phoneRegex = /^1[3-9]\d{9}$/
  293. if (!phoneRegex.test(value)) {
  294. errors[field] = '请输入正确的手机号'
  295. return false
  296. }
  297. }
  298. return true
  299. }
  300. function validateForm() {
  301. let isValid = true
  302. const requiredFields = ['name', 'type', 'address', 'customerName', 'customerPhone']
  303. for (const field of requiredFields) {
  304. if (!validateField(field)) {
  305. isValid = false
  306. }
  307. }
  308. return isValid
  309. }
  310. function addContact() {
  311. form.value.contacts.push({ name: '', role: '', phone: '' })
  312. }
  313. function removeContact(index: number) {
  314. form.value.contacts.splice(index, 1)
  315. }
  316. function addServicePoint() {
  317. form.value.servicePoints.push({ name: '', address: '', serviceType: '', contactName: '', contactPhone: '' })
  318. }
  319. function removeServicePoint(index: number) {
  320. form.value.servicePoints.splice(index, 1)
  321. }
  322. async function submitForm() {
  323. if (!validateForm()) {
  324. uni.showToast({ title: '请完善表单信息', icon: 'none' })
  325. return
  326. }
  327. submitting.value = true
  328. try {
  329. const payload = {
  330. projectName: form.value.name,
  331. projectType: form.value.type,
  332. address: [form.value.province, form.value.city, form.value.district, form.value.address].filter(Boolean).join(''),
  333. customerName: form.value.customerName,
  334. customerPhone: form.value.customerPhone,
  335. contractAmount: form.value.contractAmount ? Number(form.value.contractAmount) : undefined,
  336. contractStartDate: form.value.contractStartDate || undefined,
  337. contractEndDate: form.value.contractEndDate || undefined,
  338. serviceCycle: form.value.serviceCycle || undefined,
  339. remark: form.value.remark || undefined,
  340. contacts: form.value.contacts.filter((c) => c.name || c.phone),
  341. servicePoints: form.value.servicePoints.filter((p) => p.name || p.address),
  342. }
  343. await createProject(payload)
  344. uni.showToast({ title: '创建成功', icon: 'success' })
  345. setTimeout(() => {
  346. uni.navigateBack()
  347. }, 1500)
  348. } catch (error) {
  349. uni.showToast({ title: '创建失败', icon: 'none' })
  350. } finally {
  351. submitting.value = false
  352. }
  353. }
  354. </script>