add.vue 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242
  1. <template>
  2. <div class="add-container">
  3. <el-dialog v-model="dialogVisible" title="新建" width="800px" :close-on-click-modal="false" @close="handleClose">
  4. <el-form ref="formRef" :model="formData" :rules="rules" label-width="140px" label-position="right">
  5. <!-- 需求标题 -->
  6. <el-form-item label="需求标题" prop="requirementTitle" required>
  7. <el-input v-model="formData.requirementTitle" placeholder="请输入" maxlength="100" clearable />
  8. </el-form-item>
  9. <!-- 装修类型 -->
  10. <el-form-item label="装修类型" prop="renovationType">
  11. <el-radio-group v-model="formData.renovationType">
  12. <el-radio :label="1"> 新房装修 </el-radio>
  13. <el-radio :label="2"> 旧房改造 </el-radio>
  14. <el-radio :label="3"> 局部装修 </el-radio>
  15. </el-radio-group>
  16. </el-form-item>
  17. <!-- 房屋面积 -->
  18. <el-form-item label="房屋面积(㎡)" prop="houseArea" required>
  19. <el-input-number
  20. v-model="formData.houseArea"
  21. :min="0"
  22. :max="99999"
  23. :precision="2"
  24. placeholder="请输入"
  25. style="width: 100%"
  26. />
  27. </el-form-item>
  28. <!-- 装修预算 -->
  29. <el-form-item label="装修预算(万元)" prop="renovationBudget" required>
  30. <el-input-number
  31. v-model="formData.renovationBudget"
  32. :min="0"
  33. :max="99999"
  34. :precision="2"
  35. placeholder="请输入"
  36. style="width: 100%"
  37. />
  38. </el-form-item>
  39. <!-- 详细需求 -->
  40. <el-form-item label="详细需求" prop="detailedRequirement">
  41. <el-input
  42. v-model="formData.detailedRequirement"
  43. type="textarea"
  44. :rows="4"
  45. placeholder="请输入"
  46. maxlength="500"
  47. show-word-limit
  48. clearable
  49. />
  50. </el-form-item>
  51. <!-- 期望装修时间 -->
  52. <el-form-item label="期望装修时间" prop="expectedRenovationTime" required>
  53. <el-date-picker
  54. v-model="formData.expectedRenovationTime"
  55. type="date"
  56. placeholder="请选择"
  57. value-format="YYYY-MM-DD"
  58. style="width: 100%"
  59. :disabled-date="disabledBeforeToday"
  60. />
  61. </el-form-item>
  62. <!-- 上传房屋图纸 -->
  63. <el-form-item label="上传房屋图纸" prop="attachmentUrls" required>
  64. <el-upload
  65. v-model:file-list="fileList"
  66. action="#"
  67. list-type="picture-card"
  68. :limit="9"
  69. :on-preview="handlePictureCardPreview"
  70. :on-remove="handleRemove"
  71. :on-exceed="handleExceed"
  72. :before-upload="beforeUpload"
  73. :http-request="handleAttachmentUpload"
  74. accept="image/jpeg,image/png,image/gif,image/webp,image/bmp,video/mp4,video/quicktime,video/webm,.jpg,.jpeg,.png,.gif,.webp,.bmp,.mp4,.mov,.webm,.m4v"
  75. multiple
  76. >
  77. <template #file="{ file }">
  78. <div class="attachment-upload-item">
  79. <video
  80. v-if="isVideoUploadFile(file) && file.url"
  81. :src="file.url"
  82. :poster="getAttachmentVideoPoster(file)"
  83. class="attachment-upload-thumb"
  84. muted
  85. preload="metadata"
  86. playsinline
  87. />
  88. <img v-else-if="file.url" class="attachment-upload-thumb" :src="file.url" alt="" />
  89. <div v-else class="attachment-upload-placeholder">
  90. <el-icon><VideoPlay v-if="isVideoUploadFile(file)" /><Picture v-else /></el-icon>
  91. </div>
  92. <div class="attachment-upload-handle" @click.stop>
  93. <div class="attachment-upload-handle-icon" @click="handlePictureCardPreview(file)">
  94. <el-icon><ZoomIn /></el-icon>
  95. <span>查看</span>
  96. </div>
  97. <div class="attachment-upload-handle-icon" @click="handleAttachmentRemove(file)">
  98. <el-icon><Delete /></el-icon>
  99. <span>删除</span>
  100. </div>
  101. </div>
  102. </div>
  103. </template>
  104. <el-icon><Plus /></el-icon>
  105. </el-upload>
  106. <div class="upload-tip">支持图片或视频 ({{ fileList.length }}/9),图片单文件不超过 20MB,视频单文件不超过 200MB</div>
  107. </el-form-item>
  108. <!-- 联系人 -->
  109. <el-form-item label="联系人" prop="contactName" required>
  110. <el-input v-model="formData.contactName" placeholder="请输入" maxlength="50" clearable />
  111. </el-form-item>
  112. <!-- 联系电话 -->
  113. <el-form-item label="联系电话" prop="contactPhone" required>
  114. <el-input v-model="formData.contactPhone" placeholder="请输入" maxlength="20" clearable />
  115. </el-form-item>
  116. <!-- 所在城市 -->
  117. <el-form-item label="所在城市" prop="city" required>
  118. <el-input
  119. v-model="formData.city"
  120. placeholder="请选择"
  121. readonly
  122. @click="showCityDialog = true"
  123. clearable
  124. @clear="handleCityClear"
  125. >
  126. <template #suffix>
  127. <el-icon class="cursor-pointer" @click="showCityDialog = true">
  128. <ArrowDown />
  129. </el-icon>
  130. </template>
  131. </el-input>
  132. </el-form-item>
  133. <!-- 详细地址 -->
  134. <el-form-item label="详细地址" prop="detailedAddress" required>
  135. <el-input
  136. v-model="formData.detailedAddress"
  137. type="textarea"
  138. :rows="3"
  139. placeholder="请输入"
  140. maxlength="200"
  141. show-word-limit
  142. clearable
  143. />
  144. </el-form-item>
  145. <!-- 服务协议确认 -->
  146. <el-form-item prop="agreementConfirmed">
  147. <el-checkbox v-model="formData.agreementConfirmed" :true-label="1" :false-label="0">
  148. 我已阅读并同意
  149. <el-link type="primary" :underline="false" @click="handleShowAgreement"> 《用户服务协议》 </el-link>
  150. <el-link type="primary" :underline="false" @click="handleShowPrivacy"> 《隐私政策》 </el-link>
  151. ,允许装修商家查看我的需求信息并与我联系
  152. </el-checkbox>
  153. </el-form-item>
  154. </el-form>
  155. <template #footer>
  156. <div class="dialog-footer">
  157. <el-button @click="handleClose"> 返回 </el-button>
  158. <el-button type="primary" :loading="submitLoading" @click="handleSubmit"> 确定 </el-button>
  159. </div>
  160. </template>
  161. </el-dialog>
  162. <!-- 城市选择对话框 -->
  163. <el-dialog v-model="showCityDialog" title="选择城市" width="600px">
  164. <div class="city-selector">
  165. <el-select
  166. v-model="selectedProvince"
  167. placeholder="请选择省"
  168. clearable
  169. style="width: 100%; margin-bottom: 10px"
  170. @change="handleProvinceChange"
  171. >
  172. <el-option v-for="province in provinceOptions" :key="province.adcode" :label="province.name" :value="province.adcode" />
  173. </el-select>
  174. <el-select
  175. v-model="selectedCity"
  176. placeholder="请选择市"
  177. clearable
  178. style="width: 100%; margin-bottom: 10px"
  179. :disabled="!selectedProvince"
  180. @change="handleCitySelect"
  181. >
  182. <el-option v-for="city in cityOptions" :key="city.adcode" :label="city.name" :value="city.adcode" />
  183. </el-select>
  184. <el-select
  185. v-model="selectedDistrict"
  186. placeholder="请选择区"
  187. clearable
  188. style="width: 100%"
  189. :disabled="!selectedCity"
  190. @change="handleDistrictSelect"
  191. >
  192. <el-option v-for="district in districtOptions" :key="district.adcode" :label="district.name" :value="district.adcode" />
  193. </el-select>
  194. </div>
  195. <template #footer>
  196. <div class="dialog-footer">
  197. <el-button @click="showCityDialog = false"> 取消 </el-button>
  198. <el-button type="primary" @click="handleConfirmCity"> 确定 </el-button>
  199. </div>
  200. </template>
  201. </el-dialog>
  202. <!-- 图片预览 -->
  203. <PcImagePreviewViewer
  204. v-model:visible="imageViewerVisible"
  205. :url-list="imageViewerUrlList"
  206. :initial-index="imageViewerInitialIndex"
  207. />
  208. <PcVideoPreviewDialog v-model="videoPreviewVisible" :src="videoPreviewUrl" title="查看视频" />
  209. <!-- 用户服务协议弹窗 -->
  210. <el-dialog v-model="agreementDialogVisible" title="用户服务协议" width="800px" :close-on-click-modal="false">
  211. <div class="agreement-content">
  212. <p>
  213. 您在使用爱丽恩严(大连)商务科技有限公司旗下U店在这APP软件提供的服务前,应当仔细认真阅读本《服务条款》(下称"本条款")中的全部规则、《用户协议》及发布的其他服务条款、专项产品或服务规则或规范的内容,尤其是以粗体或加下划线标示的条款,包括但不限于免除或者限爱丽恩严(大连)商务科技有限公司责任的条款、对用户权利进行限制的条款以及约定争议解决方式、司法管辖的条款,上述条款请您重点阅读。您有权选择同意或者不同意本协议。
  214. </p>
  215. <p><strong>本协议所称的爱丽恩严</strong>是指爱丽恩严(大连)商务科技有限公司的简称。</p>
  216. <p>
  217. 您与爱丽恩严均应当严格履行本协议及其补充协议所约定的各项义务,如发生争议或者纠纷,双方可以友好协商解决;协商不成的,任何一方均可向本协议签订地有管辖权的人民法院提起诉讼。本协议签订地为大连市中山区。
  218. </p>
  219. <p>
  220. 您如果通过登录U店在哪APP用户注册页面或者爱丽恩严提供的其他用户注册渠道注册用户账号,完成我们的注册流程并通过点击同意的形式在线签署本协议即视为您完全同意本协议,愿意接受本协议所有及任何条款的约束。
  221. </p>
  222. <p>所有服务规则视为本条款不可分割的一部分,与本条款具有同等法律效力。</p>
  223. <p>
  224. <strong>本条款的签约双方</strong>为爱丽恩严服务的实际运营商爱丽恩严(大连)商务科技有限公司(下称"爱丽恩严")
  225. 与使用爱丽恩严相关服务的使用人(以下称"用户"或"您"),本条款是您与爱丽恩严之间关于您使用爱丽恩严提供的各项服务所订立的服务条款,具有正式书面合同的效力。
  226. </p>
  227. <p>
  228. 本条款为爱丽恩严平台《用户协议》(包括但不限于所附的《隐私协议》)
  229. 的必要组成部分。《用户协议》将同时适用于爱丽恩严的各项服务。如本条款与《用户协议》文本内容存在冲突之处,则以时间上最新发布的内容为准,发布时间相同的,以本条款为准。本条款有待明确、存在歧义或未规定之处均以《用户协议》中的规定为准。
  230. </p>
  231. <p>
  232. 您理解并同意,爱丽恩严将根据《用户协议》的约定,对本条款或各项服务规则不时地进行修改更新。修改更新内容的发布和实施均适用《用户协议》的相关约定。
  233. </p>
  234. <h3>一、名词解释</h3>
  235. <p>除您与爱丽恩严另有约定外,本协议及其补充协议当中的下列名词均采用如下解释:</p>
  236. <p>
  237. 1.
  238. 服务条款:即本协议,指您与爱丽恩严当下订立的旨在约定您登录、使用本平台,通过本平台下达订单、购买商品/服务、支付价款以及爱丽恩严运用自己的平台系统,通过互联网络等方式为用户提供商户信息、点评信息、消费信息、优惠信息、团购等整个网络服务过程中,您与爱丽恩严之间的权利、义务的书面合同。
  239. </p>
  240. <p>
  241. 2. 爱丽恩严:指是爱丽恩严(大连)商务科技有限公司运营的爱丽恩严生活服务信息平台,包括但不限于
  242. 爱丽恩严网站、爱丽恩严客户端、爱丽恩严小程序等形式的互联网平台。
  243. </p>
  244. <p>
  245. 3.
  246. 商户:是指根据本条款及其他适用的服务规则,在爱丽恩严平台上发布信息为爱丽恩严用户提供各项线上及线下的商品或服务的第三方商家。
  247. </p>
  248. <p>
  249. 4.
  250. 团购:团购是指通过爱丽恩严平台,一定数量的用户组团或者参团,以较低折扣价格购买同一种服务或商品的行为,包括消费买单、预约及预订服务项目等。
  251. </p>
  252. <p>
  253. 5.
  254. 团购信息:是指商户通过爱丽恩严发布的团购商品或服务的信息,此类信息包括但不限于团购商品或服务的名称、种类、数量、质量、价格、配送方式、支付形式、退换货方式、退款条件、售后服务等内容。
  255. </p>
  256. <p>
  257. 6.
  258. 团购券:是指用户通过爱丽恩严购买商户的服务或商品并成功支付团购价款后,相关商户通过爱丽恩严自动向用户出具的供用户向该商户要求提供商品或服务的交易凭证。
  259. </p>
  260. <p>
  261. 7.
  262. 优惠券:是指用户通过爱丽恩严购买商户的服务或商品时,购买、免费领取或使用积分兑换用于享受减免、折扣或其他优惠条件的电子交易凭证。
  263. </p>
  264. <p>
  265. 8. 交互信息服务:交互信息服务是指用户在爱丽恩严应用上交互平台(下称 "点评交互平台
  266. ")上发布文字、图片、视频及表演(直播)等信息。点评交互平台包括但不限于点评社区、点评头条、 点评直播、笔记功能等。
  267. </p>
  268. <h3>二、用户账号</h3>
  269. <p>
  270. 1.在注册、管理、使用账号时,您应遵循诚实信用、合法善意的原则。您向平台提交的相关注册资料应当遵守法律法规、社会主义制度、国家利益、公民合法权益、公序良俗、信息真实等原则,不应提交任何违法或不良信息。相关资料如有变动,您应及时更新。如果因您所提供的注册资料不合法、不真实、不准确或未及时更新,从而导致相关法律责任或不利后果的,您将承担相应的法律责任及不利后果。同时,爱丽恩严有权拒绝为您提供注册服务。
  271. </p>
  272. <p>2.您同意并承诺以合法、合规、合理的方式使用爱丽恩严账号:</p>
  273. <ul>
  274. <li>
  275. (1)您设置的账号昵称、头像及个人介绍等个人资料不得出现违法和不良信息,包括但不限于使用政治、色情、低俗、侮辱、诽谤等违反法律、道德及公序良俗的词语。
  276. </li>
  277. <li>
  278. (2)未经他人许可,您不得使用他人名义(包括但不限于冒用他人姓名、名称、字号、头像、身份等或采取其他足以让人引起混淆的方式)开设爱丽恩严账号。
  279. </li>
  280. <li>
  281. (3)不得假冒、仿冒、捏造党政军机关、企事业单位、新闻媒体等组织机构名称、标识信息或采取其他足以让人引起混淆的方式开设爱丽恩严账号。
  282. </li>
  283. <li>
  284. (4)您不得恶意注册爱丽恩严账号(包括但不限于频繁注册、批量注册账号等行为)或将账号用于非法或不正当用途(包括但不限于流量作假等行为),不得实施任何侵害国家利益、损害其他公民合法权益,有害社会道德风尚的行为,不得采取各种技术手段恶意绕开或者对抗平台规则。
  285. </li>
  286. </ul>
  287. <p>
  288. 3.您理解并同意,您的爱丽恩严账号的所有权及有关权益均归爱丽恩严所有,您仅享有该账号的使用权且仅限于您本人使用。为保证账号安全,未经爱丽恩严的书面同意,您不应将爱丽恩严账号以赠与、转让、出售、出借或其他方式许可他人使用,否则您应当承担由此产生的全部责任,爱丽恩严保留拒绝提供相应服务、冻结或收回注册账号或终止本服务协议的权利,并可要求您对爱丽恩严所承受的损失予以赔偿。
  289. </p>
  290. <p>
  291. 4.您应妥善保管账号信息、账号密码以及其他与账号相关的信息、资料。您有责任维护个人账号、密码的安全性与保密性,并对您以注册账号名义所从事的活动承担全部法律责任,包括但不限于您在爱丽恩严平台进行的任何信息发表、浏览点击等操作行为可能引起的一切法律责任。若发现他人未经许可使用您的账号或发生其他任何安全漏洞问题时,您应当立即通知爱丽恩严。
  292. </p>
  293. <p>
  294. 5.您理解并同意,如您违反上述条款,爱丽恩严有权对您采取要求限期改正、禁止注册、删除或屏蔽违法违规信息、不同时限的禁止信息发布和账号信息修改、封号、注销账号等处置措施。
  295. </p>
  296. <p>
  297. 6.您理解并同意,您可通过注册新的账号或登录您现有的账号以使用爱丽恩严的各项服务及功能,您使用爱丽恩严账号应同时遵守《用户协议》中的各项规定。
  298. </p>
  299. <h3>三、用户管理</h3>
  300. <p>
  301. 1.您知悉、理解并同意,爱丽恩严在服务过程中,可能涉及收集、存储、使用、共享和保护用户个人信息。在您使用爱丽恩严提供的服务时,您同意爱丽恩严依据《隐私协议》的规定执行相关个人信息的收集、使用和共享。您进一步同意,就爱丽恩严平台所产生的交易或其他与个人信息使用紧密相关的交易,您授权爱丽恩严使用或允许爱丽恩严许可的第三方在必要、合理的限度内使用您的个人信息,包括但不限于身份信息、账号信息、交易信息等。
  302. </p>
  303. <p>
  304. 2.您知悉并同意,爱丽恩严将按照有权网络信息主管部门的相关规定对您的账号信息及所发布的各项信息进行必要的安全审查及评估,保留并向有关部门定期报送您的账号信息及您所发布的各项信息。
  305. </p>
  306. <p>
  307. 3.用户有权在爱丽恩严平台上发布客观、真实、亲身体验的文字点评、图片点评、视频点评或者自行添加商户、完善爱丽恩严平台中商户的商户名称、营业时间、位置等信息。用户同意并理解,为了遵守法律法规和政策,维护良好的平台秩序,爱丽恩严有权对特定用户、特定行业、特定商品或者服务的点评信息或者商户信息发布流程作出特别设定或者限制。
  308. </p>
  309. <p>
  310. 4.您可以通过正式的页面公告和/或站内信和/或电子邮件和/或客服电话和/或手机短信、常规的信件接收和查看中奖、优惠等活动或信息,并在爱丽恩严上自行浏览、下载和使用优惠券和/或团购券。
  311. </p>
  312. <p>
  313. 5.用户知晓并同意,他人可能通过"认领"等操作获得用户在爱丽恩严平台中添加的商户条目之管理权限,并可能对用户添加、完善之信息进行修改或者使用。
  314. </p>
  315. <p>6.用户有权根据爱丽恩严相关规定,在发布点评信息等贡献后,取得爱丽恩严给予的奖励(如贡献值、积分等)。</p>
  316. <p>
  317. 7.用户在发布信息时,您应确保该等信息的真实性、客观性、合法性,爱丽恩严提倡用户贡献高质量点评。用户应确保在爱丽恩严上发表的各类点评信息、攻略或文章、图片及视频均不涉及侵犯第三方隐私、著作权或其他合法权益。用户需维护点评的客观性,不得利用爱丽恩严用户身份进行任何违反法律、法规、国家政策以及诚实信用原则的行为,包括但不限于:
  318. </p>
  319. <ul>
  320. <li>(1)违反法律法规及国家政策的行为:</li>
  321. <li>①反对宪法所确定的基本原则的;</li>
  322. <li>②危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的;</li>
  323. <li>③损害国家荣誉和利益的;</li>
  324. <li>④歪曲、丑化、亵渎、否定英雄烈士事迹和精神,以侮辱、诽谤或者其他方式侵害英雄烈士的姓名、肖像、名誉、荣誉的;</li>
  325. <li>⑤宣扬恐怖主义、极端主义或者煽动实施恐怖活动、极端主义活动的;</li>
  326. <li>⑥煽动民族仇恨、民族歧视,破坏民族团结的;</li>
  327. <li>⑦破坏国家宗教政策,宣扬邪教和封建迷信的;</li>
  328. <li>⑧散布谣言,扰乱经济秩序和社会秩序的;</li>
  329. <li>⑨散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的;</li>
  330. <li>⑩侮辱或者诽谤他人,侵害他人名誉、隐私和其他合法权益的;</li>
  331. <li>⑪法律、行政法规禁止的其他内容。</li>
  332. <li>(2)违反诚实信用原则的行为:</li>
  333. <li>①炒作并向商户收取费用或获取利益;</li>
  334. <li>②为获得利益或好处,参与或组织撰写及发布虚假点评;</li>
  335. <li>③以差评威胁、要求商户提供额外的利益或好处;</li>
  336. <li>④以虚构事实、侮辱、诽谤等方式恶意诋毁爱丽恩严网或商家的商誉;</li>
  337. <li>⑤进行其他其它影响点评客观、干扰扰乱爱丽恩严正常秩序的违规行为等。</li>
  338. </ul>
  339. <p>如您存在以上行为的,爱丽恩严有权采取如下行动:</p>
  340. <ul>
  341. <li>①屏蔽违规信息,视情节轻重情况发出警告;</li>
  342. <li>②清除其所有评价、图片,屏蔽其发布的涉及商业性炒作的帖子;</li>
  343. <li>③暂时限制/永久限制您使用账号在爱丽恩严平台上发布信息;</li>
  344. <li>④法律、行政法规、本条款及《用户协议》规定的其他方式。</li>
  345. </ul>
  346. <p>
  347. 8.禁止用户将爱丽恩严以任何形式作为从事各种非法活动的场所、平台或媒介。未经爱丽恩严的授权或许可,用户不得借用爱丽恩严的名义从事任何商业活动,也不得以任何形式将爱丽恩严作为从事商业活动的场所、平台或媒介。
  348. </p>
  349. <p>
  350. 9.您违反本条款、《用户协议》或爱丽恩严发布的其他任何服务规则,则爱丽恩严有权在法律允许的范围内采取一切必要的措施,包括但不限于删除用户发布的内容、取消用户在爱丽恩严获得的用户积分、星级、荣誉以及虚拟财富,暂停或终止您对爱丽恩严账号的使用,给爱丽恩严造成损失的,您应负全部赔偿责任,包括且不限于财产损害赔偿、名誉损害赔偿、诉讼费、律师费、公证费、交通费等因维权而产生的合理费用。爱丽恩严有权按照本条款及《用户协议》的相关规定对您的行为进行处理。
  351. </p>
  352. <h3>四、团购服务特别约定</h3>
  353. <p>1.您有权通过爱丽恩严平台浏览各项团购信息,购买团购券、优惠券并使用其参与相关商户提供的商品或服务。</p>
  354. <p>
  355. 2.您知悉并同意,您在爱丽恩严平台上所浏览及购买的团购券、优惠券或其他消费产品均系商户所提供的商品或服务,如您因购买或使用该等商品或服务产生任何问题或争议,您应自行与相应商户协商解决。
  356. </p>
  357. <h3>五、信息评价等特别约定</h3>
  358. <p>
  359. 1.您应确保具有相应的资格并符合在爱丽恩严开展信息评价的相应条件。
  360. 如果因为您不具备相应资格或者条件的情形下通过爱丽恩严平台发布信息而引发任何法律责任,您应自行承担。因此给爱丽恩严平台造成损失的,您应予以全额赔偿。
  361. </p>
  362. <p>
  363. 2.平台有可能在相关法律法规和政策的指引下,对评价、帖子等信息发布实施分级分类管理,建立发布者信用等级管理体系,建立黑名单管理制度等。
  364. </p>
  365. <p>3.用户在平台发布的文字、图片、视频、表演(直播)等信息均应遵守相关法律法规、规章、本条款及《用户协议》的相关规定。</p>
  366. <p>4.您同意:如出现以下任一情形的,爱丽恩严将视情节轻重采取删除信息、限制平台使用功能、停止您的账号使用等处理措施:</p>
  367. <ul>
  368. <li>(1)发布违反相关法律法规、规章、政策之规定或者违反本条款及相关服务规则的信息;</li>
  369. <li>(2)发布与主题无关的信息或评论,包含文字、图片、视频等;</li>
  370. <li>(3)发布可能存在交易风险的外部网站和APP信息,如发布社交、团购等外部网站或 APP的名称、超链接、二维码等信息;</li>
  371. <li>
  372. (4)发布的视频违反国家法律法规的相关要求,包括但不限于《广播电视管理条例》、《电影管理条例》、《互联网视听节目服务管理规定》、《广电总局关于加强互联网视听节目内容管理的通知》等的相关要求;
  373. </li>
  374. <li>
  375. (5)用户进行表演(直播)时违反国家法律法规的相关要求,包括但不限于《互联网文化管理暂行规定》、《互联网直播服务管理规定》等的相关要求;
  376. </li>
  377. <li>(6)其他违反及《用户协议》相关规定的情形。</li>
  378. </ul>
  379. <h3>六、关于服务终止的约定</h3>
  380. <p>
  381. 1.您同意爱丽恩严有权随时修改或中断其向您提供的任何免费服务而无需事先通知您。您与爱丽恩严进行的有偿交易,您同意爱丽恩严有权在事先通知的情况下予以修改、中断,并按照公平、诚实信用、等价有偿的原则处理后续事宜。
  382. </p>
  383. <p>
  384. 2.如您的账号同时符合以下条件,则爱丽恩严有权利终止您对爱丽恩严账号的使用。这将导致您的账号不能再登录爱丽恩严,相应服务亦同时终止:
  385. </p>
  386. <ul>
  387. <li>(1)连续六个月未登录;</li>
  388. <li>(2)不存在未到期的有效业务;</li>
  389. <li>(3)终止您的账号和服务的行为不违反相关法律法规的强制性规定。</li>
  390. </ul>
  391. <p>
  392. 3.如您对本条款及其不时修订有任何异议的,您有权停止使用爱丽恩严的各项服务,或通过客服等渠道告知爱丽恩严停止对您提供服务。停止服务后,除法律法规另有明确规定外,爱丽恩严有权(但无义务)保留您的账号访问爱丽恩严的相关信息和数据,或留存、转发任何账号内的任何站内信或短消息。在此情况下,爱丽恩严没有义务(但有权利)向您或代为向商家或其他第三方传送任何未处理的信息或未完成的服务或交易信息。您同意爱丽恩严不就终止爱丽恩严服务而对您或任何第三方承担任何责任。
  393. </p>
  394. <p>
  395. 4.您同意,您与爱丽恩严的合同关系终止后,爱丽恩严就您在使用爱丽恩严服务期间存在违法行为或违反本条款和/或其他服务规则的行为的,
  396. 仍可依据本条款向您主张权利。
  397. </p>
  398. <h3>七、知识产权及其它权利</h3>
  399. <p>1.用户确认其已经仔细阅读并同意爱丽恩严关于知识产权等相关权利的一切声明。</p>
  400. <p>
  401. 2.爱丽恩严平台及相关服务的运营系统由爱丽恩严自主开发、运营并提供技术支持,爱丽恩严对平台服务的开发和运营等过程中产生的所有数据和信息等享有全部权利。爱丽恩严提供各项服务时所依托软件的著作权、专利权,所使用的各项商标、商业形象、商业标识、技术诀窍,其著作权、商标权及其他各项相关权利均归爱丽恩严所有。
  402. </p>
  403. <p>
  404. 3,您理解并同意,您在爱丽恩严发表的各项点评信息、文章、视频、反馈意见等所有信息及其衍生品的知识产权及所有权,适用《用户协议》中的相关约定。
  405. </p>
  406. <h3>八、免责事由</h3>
  407. <p>
  408. 1.除非爱丽恩严以书面形式明确约定,爱丽恩严对于用户以任何方式(包括但不限于包含、经由、连接或下载)从爱丽恩严所获得的任何由商户发布的内容信息,包括但不限于商户信息、点评内容等,不保证其准确性、完整性、可靠性。用户应当自行审核判断相关信息,并对于用户因爱丽恩严上的内容信息而购买、获取的任何产品、服务、信息或资料自行承担责任和风险。用户因此受损的,爱丽恩严在法律允许的最大范围内予以免责。爱丽恩严内所有用户所发表的用户点评、商户或者产品信息等,仅代表用户个人观点,并不表示爱丽恩严赞同其观点或证实其描述,爱丽恩严在法律允许的最大范围内予以免责。
  409. </p>
  410. <p>
  411. 2.您同意并理解,针对商户向您销售/提供的商品/服务,爱丽恩严并非您所购买的具体商品或者服务的生产者和销售者。您同意,针对该等商品或者服务以及相关售后服务中所产生的任何矛盾和纠纷均不应针对爱丽恩严提出。在法律允许的范围内,爱丽恩严不对上述商品/服务承担包括解释说明、赔偿在内的任何责任。
  412. </p>
  413. <p>
  414. 3.您在爱丽恩严平台上传、发布任何信息或者内容的,应当自行保留备份。爱丽恩严不对用户所发布信息的保存、修改、删除或储存失败负责,对爱丽恩严上的非因爱丽恩严故意所导致的排字错误、疏忽等不承担责任。爱丽恩严有权但无义务,改善或更正爱丽恩严任何部分之疏漏、错误。
  415. </p>
  416. <p>
  417. 4.您知悉爱丽恩严在其平台上所公布的各项排行榜单系根据用户真实评价、访问次数、浏览时长等数据自行生成,并无人工干预。该等榜单仅为您选择商品或服务提供参考,不涉及任何商户付费宣传,您应自行判断榜单中所列商品及服务的属性,爱丽恩严将不对此榜单中所列商品和服务做出任何形式的担保或保证。
  418. </p>
  419. <p>5.任何非经爱丽恩严正规渠道获得的中奖、优惠等活动或信息,爱丽恩严不承担法律责任。</p>
  420. <p>
  421. 6.爱丽恩严因发现爱丽恩严上显现的团购、预订、消费买单等交易产品信息明显错误或缺货时,有权单方面作出修改,但该等修改不应视为爱丽恩严对您作出的任何承诺。
  422. </p>
  423. <p>
  424. 7.爱丽恩严有权在法律允许范围内,在不通知您的情况下删除任何用户发布的不符合本条款及各项服务规则、《用户协议》以及法律规定的信息。爱丽恩严将不对删除该等信息给您造成的不便或损失承担任何责任。
  425. </p>
  426. <p>
  427. 8.您同意,在法律许可范围内,爱丽恩严在任何情况下都不对任何个人或实体的直接、间接、偶然、特殊、惩罚性的损害或其他损害或损失承担责任,这些损害或损失包括但不限于:
  428. </p>
  429. <ul>
  430. <li>(1)您理解并同意,由于互联网的特殊性造成的爱丽恩严显示的信息所存在的滞后性或差错,爱丽恩严对此不承担任何责任;</li>
  431. <li>
  432. (2)您在使用爱丽恩严服务中,因第三方原因使您遭受侮辱、诽谤、不作为、淫秽、色情或亵渎事件,爱丽恩严在法律允许的范围内不承担法律责任;
  433. </li>
  434. <li>(3)《用户协议》中规定其他不可抗力及免责事由。</li>
  435. </ul>
  436. <h3>九、管辖、法律适用与争议解决</h3>
  437. <p>1.本协议的成立、生效、履行、解释与纠纷解决,适用中华人民共和国法律法规,并且排除一切冲突法规定的适用。</p>
  438. <p>
  439. 2.您同意并理解,如您因通过爱丽恩严平台购买的任何商品或者服务产生任何纠纷的,爱丽恩严可在法律法规要求的范围内协助您与争议对方进行协商调解。您同意,爱丽恩严有权向争议双方了解情况,并将所了解的情况通过必要方式通知对方。但您理解并同意,爱丽恩严无任何义务对您与商家之间的任何争议承担任何责任。
  440. </p>
  441. <p>
  442. 3.您与爱丽恩严均应当严格履行本协议及其补充协议所约定的各项义务,如发生争议或者纠纷,双方可以友好协商解决;协商不成的,任何一方均可向本协议签订地有管辖权的法院提起诉讼。
  443. </p>
  444. <p>本协议签订地:大连市中山区。</p>
  445. <h3>十、意见及反馈</h3>
  446. <p>如您对本协议有任何问题或建议,请在工作时间联系爱丽恩严客服部门。联系方式如下:</p>
  447. <ul>
  448. <li>您可以通过网站(ailien.shop)/App上提供的在线联系方式/客服系统与我们联系;</li>
  449. </ul>
  450. </div>
  451. <template #footer>
  452. <div class="dialog-footer-center">
  453. <el-button type="primary" @click="agreementDialogVisible = false"> 已阅读并同意协议内容 </el-button>
  454. </div>
  455. </template>
  456. </el-dialog>
  457. <!-- 隐私政策弹窗 -->
  458. <el-dialog v-model="privacyDialogVisible" title="隐私政策" width="800px" :close-on-click-modal="false">
  459. <div class="agreement-content">
  460. <p>生效日期:2024 年 10 月 29 日</p>
  461. <p>
  462. 欢迎使用[U店在这]!我们深知个人信息对您的重要性,并将竭尽全力保护您的隐私。本隐私协议旨在向您说明我们如何收集、使用、存储和保护您的个人信息,以及您对个人信息享有的权利。
  463. </p>
  464. <p>一、定义</p>
  465. <p>
  466. 1."个人信息"是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。
  467. </p>
  468. <p>2."我们"、"本公司" 或 "[U店在这]" 指 [APP 所属公司名称]。</p>
  469. <p>二、信息收集范围</p>
  470. <p>3.您在注册、登录 [U店在这] 时提供的信息,包括但不限于用户名、密码、手机号码、电子邮箱等。</p>
  471. <p>4.您在使用 [U店在这] 过程中产生的信息,如浏览记录、搜索记录、交易记录等。</p>
  472. <p>5.我们从第三方获取的信息,如您通过第三方平台登录 [U店在这] 时,我们可能会获取您在该第三方平台上的部分信息。</p>
  473. <p>三、信息使用目的</p>
  474. <p>6.为您提供个性化的服务和内容,满足您的需求。</p>
  475. <p>7.改善和优化 [U店在这] 的功能和体验。</p>
  476. <p>8.进行数据分析和研究,以提升我们的服务质量和效率。</p>
  477. <p>9.与您进行沟通和互动,回复您的咨询和反馈。</p>
  478. <p>10.遵守法律法规的要求,履行我们的法律义务。</p>
  479. <p>四、信息存储</p>
  480. <p>11.我们将按照法律法规的要求,将您的个人信息存储在安全的服务器上。</p>
  481. <p>12.我们会采取合理的技术和管理措施,确保您的个人信息的安全,防止其被未经授权的访问、披露、使用、修改或丢失。</p>
  482. <p>
  483. 13.我们将根据您的使用情况和需求,确定个人信息的存储期限。在存储期限届满后,我们将对您的个人信息进行删除或匿名化处理。
  484. </p>
  485. <p>五、信息共享与披露</p>
  486. <p>14.我们不会向第三方出售、出租或交易您的个人信息。</p>
  487. <p>
  488. 15.我们可能会在以下情况下与第三方共享您的个人信息: 在获得您的明确同意后;
  489. 为了向您提供特定的服务,我们需要与第三方合作,此时我们会与该第三方签订保密协议,确保其按照本隐私协议的要求处理您的个人信息;
  490. 为了遵守法律法规的要求,履行我们的法律义务。
  491. </p>
  492. <p>
  493. 16.在以下情况下,我们可能会披露您的个人信息: 经您明确同意或授权; 根据法律法规的要求,向有权机关披露;
  494. 为了维护我们的合法权益,如在涉及诉讼、仲裁等法律程序时。
  495. </p>
  496. <p>六、您的权利</p>
  497. <p>17.您有权访问、更正、删除您的个人信息。您可以通过 [U店在这] 的设置页面或联系我们的客服来行使这些权利。</p>
  498. <p>18.您有权撤回您对我们处理您个人信息的同意。但请注意,撤回同意可能会影响您使用 [U店在这] 的部分功能。</p>
  499. <p>19.您有权要求我们提供关于我们处理您个人信息的详细信息,包括处理目的、处理方式、处理的个人信息种类等。</p>
  500. <p>
  501. 20.您有权要求我们限制对您个人信息的处理,例如在您认为我们处理您个人信息的行为违反法律法规的要求或您的合法权益受到侵害时。
  502. </p>
  503. <p>七、Cookie 和同类技术的使用</p>
  504. <p>21.我们可能会使用 Cookie 和同类技术来收集和存储您的信息,以便为您提供更好的服务和体验。</p>
  505. <p>
  506. 22.您可以通过设置浏览器的隐私设置来拒绝接受 Cookie 和同类技术。但请注意,拒绝接受 Cookie和同类技术可能会影响您使用
  507. [U店在这] 的部分功能。
  508. </p>
  509. <p>八、未成年人保护</p>
  510. <p>23.[U店在这] 不面向未成年人提供服务。如果您是未成年人,请不要使用 [U店在这]。</p>
  511. <p>24.如果我们发现我们在未经授权的情况下收集了未成年人的个人信息,我们将立即采取措施删除该信息。</p>
  512. <p>九、隐私政策的变更</p>
  513. <p>25.我们可能会根据法律法规的要求、业务发展的需要或用户反馈等因素,对本隐私协议进行修订。</p>
  514. <p>
  515. 26.修订后的隐私协议将在 [U店在这]上公布,并在公布后的特定时间生效。如果您在修订后的隐私协议生效后继续使用
  516. [U店在这],则视为您同意修订后的隐私协议。
  517. </p>
  518. <p>十、联系方式</p>
  519. <p>如果您对本隐私协议有任何疑问、意见或建议,请通过以下方式与我们联系:</p>
  520. <p>[爱丽恩(大连)贸易有限公司]</p>
  521. <p>[辽宁省大连市中山区普照街44号国泰港汇中心一层F109-110、112-117、FGQ103号商铺]</p>
  522. <p>[0411-81820856]</p>
  523. <p>[15641179898@163.com]</p>
  524. <p>我们将尽快回复您的咨询和反馈。</p>
  525. <p>感谢您对 [U店在这]的信任和支持!</p>
  526. </div>
  527. <template #footer>
  528. <div class="dialog-footer-center">
  529. <el-button type="primary" @click="privacyDialogVisible = false"> 已阅读并同意协议内容 </el-button>
  530. </div>
  531. </template>
  532. </el-dialog>
  533. </div>
  534. </template>
  535. <script setup lang="ts" name="decorationAdd">
  536. import { ref, reactive, onMounted } from "vue";
  537. import { useRoute, useRouter } from "vue-router";
  538. import {
  539. ElMessage,
  540. type FormInstance,
  541. type UploadFile,
  542. type UploadProps,
  543. type UploadRequestOptions,
  544. type UploadUserFile
  545. } from "element-plus";
  546. import { Plus, ArrowDown, Delete, ZoomIn, VideoPlay, Picture } from "@element-plus/icons-vue";
  547. import { saveOrUpdateDecoration, getDistrict } from "@/api/modules/storeDecoration";
  548. import { uploadFileToOss } from "@/api/upload.js";
  549. import { useSimpleUploadOverlayStore } from "@/stores/modules/simpleUploadOverlay";
  550. import { localGet } from "@/utils";
  551. import PcImagePreviewViewer from "@/components/pcMediaPreview/PcImagePreviewViewer.vue";
  552. import PcVideoPreviewDialog from "@/components/pcMediaPreview/PcVideoPreviewDialog.vue";
  553. const route = useRoute();
  554. const router = useRouter();
  555. const dialogVisible = ref(true);
  556. const formRef = ref<FormInstance>();
  557. const submitLoading = ref(false);
  558. const formData = reactive({
  559. requirementTitle: "",
  560. renovationType: 1,
  561. houseArea: null as number | null,
  562. renovationBudget: null as number | null,
  563. detailedRequirement: "",
  564. expectedRenovationTime: "",
  565. attachmentUrls: [] as string[],
  566. contactName: "",
  567. contactPhone: "",
  568. city: "",
  569. cityAdcode: "",
  570. detailedAddress: "",
  571. agreementConfirmed: 0,
  572. storeId: localGet("createdId") || 0
  573. });
  574. const fileList = ref<UploadFile[]>([]);
  575. /** 多选时 el-upload 会并发触发 http-request;串行化 + 整批共用一个全局上传弹层 */
  576. let sequentialUploadTail = Promise.resolve();
  577. let pendingBatchUploadCount = 0;
  578. let batchHadUploadError = false;
  579. function overlaySleep(ms: number) {
  580. return new Promise<void>(resolve => setTimeout(resolve, ms));
  581. }
  582. async function finishBatchUploadOverlay() {
  583. const overlay = useSimpleUploadOverlayStore();
  584. if (batchHadUploadError) {
  585. overlay.dismiss();
  586. } else {
  587. if (overlay.show && overlay.percent < 100) {
  588. overlay.completeSuccess();
  589. }
  590. await overlaySleep(420);
  591. overlay.dismiss();
  592. }
  593. }
  594. const imageViewerVisible = ref(false);
  595. const imageViewerUrlList = ref<string[]>([]);
  596. const imageViewerInitialIndex = ref(0);
  597. const videoPreviewVisible = ref(false);
  598. const videoPreviewUrl = ref("");
  599. const ATTACHMENT_IMAGE_MAX_MB = 20;
  600. const ATTACHMENT_VIDEO_MAX_MB = 200;
  601. function isVideoUploadFile(file: UploadFile) {
  602. const resp = file.response as { isVideo?: boolean } | undefined;
  603. if (resp?.isVideo) return true;
  604. const raw = file.raw as File | undefined;
  605. if (raw && String(raw.type || "").startsWith("video/")) return true;
  606. const url = String(file.url || "");
  607. return /\.(mp4|mov|m4v|webm|3gp|ogg|avi)(\?|#|$)/i.test(url);
  608. }
  609. function isImageUploadFile(file: File) {
  610. const mime = String(file.type || "").toLowerCase();
  611. if (mime.startsWith("image/")) return true;
  612. return /\.(jpe?g|png|gif|webp|bmp)(\?.*)?$/i.test(file.name || "");
  613. }
  614. function isVideoUploadRawFile(file: File) {
  615. const mime = String(file.type || "").toLowerCase();
  616. if (mime.startsWith("video/")) return true;
  617. return /\.(mp4|m4v|webm|ogg|mov|avi|3gp)(\?.*)?$/i.test(file.name || "");
  618. }
  619. /** OSS 视频首帧封面(列表缩略图) */
  620. function ossVideoSnapshotPosterUrl(videoUrl: string): string {
  621. const u = String(videoUrl || "").trim();
  622. if (!u || /^blob:/i.test(u) || /^data:/i.test(u)) return "";
  623. if (!/\.(mp4|mov|m4v|webm|3gp)(\?|#|$)/i.test(u)) return "";
  624. const sep = u.includes("?") ? "&" : "?";
  625. return `${u}${sep}x-oss-process=video/snapshot,t_0,f_jpg,w_400,h_400,m_fast`;
  626. }
  627. function getAttachmentVideoPoster(file: UploadFile): string | undefined {
  628. const cover = (file as UploadFile & { coverUrl?: string }).coverUrl;
  629. if (cover) return cover;
  630. const url = String(file.url || "");
  631. const poster = ossVideoSnapshotPosterUrl(url);
  632. return poster || undefined;
  633. }
  634. // 协议和隐私政策弹窗
  635. const agreementDialogVisible = ref(false);
  636. const privacyDialogVisible = ref(false);
  637. // 城市选择相关
  638. const showCityDialog = ref(false);
  639. const selectedProvince = ref("");
  640. const selectedCity = ref("");
  641. const selectedDistrict = ref("");
  642. const provinceOptions = ref<any[]>([]);
  643. const cityOptions = ref<any[]>([]);
  644. const districtOptions = ref<any[]>([]);
  645. const selectedProvinceName = ref("");
  646. const selectedCityName = ref("");
  647. const selectedDistrictName = ref("");
  648. // 禁用「当天之前」的日期(核心方法)
  649. const disabledBeforeToday = date => {
  650. // 初始化今天的日期(重置时分秒为0,避免时间戳干扰)
  651. const today = new Date();
  652. today.setHours(0, 0, 0, 0);
  653. // 核心判断:日期 < 今天 → 禁用;今天及以后 → 可用
  654. return date < today;
  655. };
  656. // 表单验证规则
  657. const rules = reactive({
  658. requirementTitle: [{ required: true, message: "请输入需求标题", trigger: "blur" }],
  659. houseArea: [{ required: true, message: "请输入房屋面积", trigger: "blur" }],
  660. renovationBudget: [{ required: true, message: "请输入装修预算", trigger: "blur" }],
  661. expectedRenovationTime: [{ required: true, message: "请选择期望装修时间", trigger: "change" }],
  662. attachmentUrls: [
  663. {
  664. required: true,
  665. validator: (rule: any, value: any, callback: any) => {
  666. // 检查所有文件是否都已上传成功
  667. const successFiles = fileList.value.filter((file: UploadFile) => file.status === "success");
  668. if (successFiles.length === 0) {
  669. callback(new Error("请上传房屋图纸"));
  670. } else {
  671. callback();
  672. }
  673. },
  674. trigger: "change"
  675. }
  676. ],
  677. contactName: [{ required: true, message: "请输入联系人", trigger: "blur" }],
  678. contactPhone: [
  679. { required: true, message: "请输入联系电话", trigger: "blur" },
  680. { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
  681. ],
  682. city: [{ required: true, message: "请选择所在城市", trigger: "change" }],
  683. detailedAddress: [{ required: true, message: "请输入详细地址", trigger: "blur" }],
  684. agreementConfirmed: [
  685. {
  686. required: true,
  687. validator: (rule: any, value: any, callback: any) => {
  688. if (formData.agreementConfirmed !== 1) {
  689. callback(new Error("请阅读并同意用户服务协议和隐私政策"));
  690. } else {
  691. callback();
  692. }
  693. },
  694. trigger: "change"
  695. }
  696. ]
  697. });
  698. // 获取省份数据
  699. const getProvinceData = async () => {
  700. try {
  701. const res: any = await getDistrict();
  702. if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
  703. const chinaData = res.data.districts[0];
  704. if (chinaData && chinaData.districts && Array.isArray(chinaData.districts)) {
  705. provinceOptions.value = chinaData.districts;
  706. }
  707. }
  708. } catch (error) {
  709. console.error("获取省份数据失败:", error);
  710. }
  711. };
  712. // 省份变化时获取城市数据
  713. const handleProvinceChange = async (provinceCode: string) => {
  714. selectedCity.value = "";
  715. selectedDistrict.value = "";
  716. cityOptions.value = [];
  717. districtOptions.value = [];
  718. selectedCityName.value = "";
  719. selectedDistrictName.value = "";
  720. if (!provinceCode) {
  721. selectedProvinceName.value = "";
  722. return;
  723. }
  724. const province = provinceOptions.value.find(p => p.adcode === provinceCode);
  725. selectedProvinceName.value = province ? province.name : "";
  726. try {
  727. const res: any = await getDistrict({ adCode: provinceCode });
  728. if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
  729. const provinceData = res.data.districts[0];
  730. if (provinceData && provinceData.districts && Array.isArray(provinceData.districts)) {
  731. cityOptions.value = provinceData.districts;
  732. }
  733. }
  734. } catch (error) {
  735. console.error("获取城市数据失败:", error);
  736. }
  737. };
  738. // 城市变化时获取区县数据
  739. const handleCitySelect = async (cityCode: string) => {
  740. selectedDistrict.value = "";
  741. districtOptions.value = [];
  742. selectedDistrictName.value = "";
  743. if (!cityCode) {
  744. selectedCityName.value = "";
  745. return;
  746. }
  747. const city = cityOptions.value.find(c => c.adcode === cityCode);
  748. selectedCityName.value = city ? city.name : "";
  749. try {
  750. const res: any = await getDistrict({ adCode: cityCode });
  751. if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
  752. const cityData = res.data.districts[0];
  753. if (cityData && cityData.districts && Array.isArray(cityData.districts)) {
  754. districtOptions.value = cityData.districts;
  755. }
  756. }
  757. } catch (error) {
  758. console.error("获取区县数据失败:", error);
  759. }
  760. };
  761. // 区县选择
  762. const handleDistrictSelect = (districtCode: string) => {
  763. if (!districtCode) {
  764. selectedDistrictName.value = "";
  765. return;
  766. }
  767. const district = districtOptions.value.find(d => d.adcode === districtCode);
  768. selectedDistrictName.value = district ? district.name : "";
  769. };
  770. // 确认城市选择
  771. const handleConfirmCity = () => {
  772. if (!selectedProvince.value || !selectedCity.value) {
  773. ElMessage.warning("请至少选择省和市");
  774. return;
  775. }
  776. // 构建城市名称:省+市+区(如果有区)
  777. let cityName = selectedProvinceName.value + selectedCityName.value;
  778. if (selectedDistrictName.value) {
  779. cityName += selectedDistrictName.value;
  780. }
  781. formData.city = cityName;
  782. formData.cityAdcode = selectedDistrict.value || selectedCity.value || selectedProvince.value;
  783. showCityDialog.value = false;
  784. };
  785. // 清除城市选择
  786. const handleCityClear = () => {
  787. formData.city = "";
  788. formData.cityAdcode = "";
  789. selectedProvince.value = "";
  790. selectedCity.value = "";
  791. selectedDistrict.value = "";
  792. selectedProvinceName.value = "";
  793. selectedCityName.value = "";
  794. selectedDistrictName.value = "";
  795. cityOptions.value = [];
  796. districtOptions.value = [];
  797. };
  798. // 房屋图纸上传前验证(图片 / 视频)
  799. const beforeUpload: UploadProps["beforeUpload"] = (rawFile: File) => {
  800. const isVideo = isVideoUploadRawFile(rawFile);
  801. const isImage = isImageUploadFile(rawFile);
  802. if (!isImage && !isVideo) {
  803. ElMessage.error("仅支持上传图片或视频文件");
  804. return false;
  805. }
  806. const sizeMb = rawFile.size / 1024 / 1024;
  807. if (isVideo) {
  808. if (sizeMb > ATTACHMENT_VIDEO_MAX_MB) {
  809. ElMessage.error(`视频大小不能超过 ${ATTACHMENT_VIDEO_MAX_MB}MB`);
  810. return false;
  811. }
  812. return true;
  813. }
  814. if (sizeMb > ATTACHMENT_IMAGE_MAX_MB) {
  815. ElMessage.error(`图片大小不能超过 ${ATTACHMENT_IMAGE_MAX_MB}MB`);
  816. return false;
  817. }
  818. return true;
  819. };
  820. // 附件上传:OSS 直传(@/api/upload.js);多文件一次 loading、请求串行
  821. const handleAttachmentUpload = (options: UploadRequestOptions): Promise<void> => {
  822. const uploadFileItem = options.file as UploadUserFile;
  823. const raw = uploadFileItem.raw || uploadFileItem;
  824. const file = raw instanceof File ? raw : null;
  825. if (!file) {
  826. ElMessage.error("文件对象不存在");
  827. return Promise.resolve();
  828. }
  829. if (pendingBatchUploadCount === 0) {
  830. batchHadUploadError = false;
  831. }
  832. pendingBatchUploadCount++;
  833. if (pendingBatchUploadCount === 1) {
  834. useSimpleUploadOverlayStore().beginUpload();
  835. }
  836. const runOne = async (): Promise<void> => {
  837. uploadFileItem.status = "uploading";
  838. uploadFileItem.percentage = 0;
  839. if (isVideoUploadRawFile(file) && !uploadFileItem.url) {
  840. uploadFileItem.url = URL.createObjectURL(file);
  841. }
  842. try {
  843. const isVideo = isVideoUploadRawFile(file);
  844. const fileUrl = await uploadFileToOss(file, isVideo ? "video" : "image", {
  845. skipSimpleUploadOverlay: true as const
  846. });
  847. if (!fileUrl) {
  848. throw new Error("上传失败,未返回文件地址");
  849. }
  850. uploadFileItem.status = "success";
  851. uploadFileItem.percentage = 100;
  852. if (uploadFileItem.url && uploadFileItem.url.startsWith("blob:")) {
  853. URL.revokeObjectURL(uploadFileItem.url);
  854. }
  855. uploadFileItem.url = fileUrl;
  856. uploadFileItem.response = { url: fileUrl, isVideo };
  857. if (isVideo) {
  858. (uploadFileItem as UploadUserFile & { coverUrl?: string }).coverUrl = ossVideoSnapshotPosterUrl(fileUrl);
  859. }
  860. if (!formData.attachmentUrls.includes(fileUrl)) {
  861. formData.attachmentUrls.push(fileUrl);
  862. }
  863. options.onSuccess?.({ code: 200, data: fileUrl, url: fileUrl });
  864. formRef.value?.validateField("attachmentUrls");
  865. } catch (error: any) {
  866. batchHadUploadError = true;
  867. console.error("文件上传失败:", error);
  868. uploadFileItem.status = "fail";
  869. if (uploadFileItem.url && uploadFileItem.url.startsWith("blob:")) {
  870. URL.revokeObjectURL(uploadFileItem.url);
  871. }
  872. const index = fileList.value.findIndex(f => f.uid === uploadFileItem.uid);
  873. if (index > -1) {
  874. fileList.value.splice(index, 1);
  875. }
  876. if (error?.message === "上传失败,未返回文件地址") {
  877. ElMessage.error(error.message);
  878. }
  879. options.onError?.(error);
  880. throw error;
  881. } finally {
  882. pendingBatchUploadCount--;
  883. if (pendingBatchUploadCount === 0) {
  884. await finishBatchUploadOverlay();
  885. }
  886. }
  887. };
  888. const p = sequentialUploadTail.then(runOne);
  889. sequentialUploadTail = p.catch(() => {
  890. /* 保持队列继续,不因单张失败阻断后续文件 */
  891. });
  892. return p;
  893. };
  894. // 附件预览(图片 / 视频)
  895. const handlePictureCardPreview = (file: UploadFile) => {
  896. if (!file.url) return;
  897. if (isVideoUploadFile(file)) {
  898. videoPreviewUrl.value = file.url;
  899. videoPreviewVisible.value = true;
  900. return;
  901. }
  902. const imageFiles = fileList.value.filter(item => item.url && !isVideoUploadFile(item));
  903. imageViewerUrlList.value = imageFiles.map(item => item.url || "");
  904. imageViewerInitialIndex.value = imageFiles.findIndex(item => item.uid === file.uid);
  905. if (imageViewerInitialIndex.value < 0) imageViewerInitialIndex.value = 0;
  906. imageViewerVisible.value = true;
  907. };
  908. const handleAttachmentRemove = (file: UploadFile) => {
  909. const index = fileList.value.findIndex(f => f.uid === file.uid);
  910. if (index > -1) {
  911. fileList.value.splice(index, 1);
  912. }
  913. handleRemove(file);
  914. };
  915. // 删除附件
  916. const handleRemove = (file: UploadFile) => {
  917. if (file.url) {
  918. const index = formData.attachmentUrls.indexOf(file.url);
  919. if (index > -1) {
  920. formData.attachmentUrls.splice(index, 1);
  921. }
  922. }
  923. formRef.value?.validateField("attachmentUrls");
  924. };
  925. // 超出限制
  926. const handleExceed = () => {
  927. ElMessage.warning("最多只能上传 9 个文件");
  928. };
  929. // 显示服务协议
  930. const handleShowAgreement = () => {
  931. agreementDialogVisible.value = true;
  932. };
  933. // 显示隐私政策
  934. const handleShowPrivacy = () => {
  935. privacyDialogVisible.value = true;
  936. };
  937. // 提交表单
  938. const handleSubmit = async () => {
  939. if (!formRef.value) return;
  940. await formRef.value.validate(async valid => {
  941. if (!valid) return;
  942. submitLoading.value = true;
  943. try {
  944. // 确保 attachmentUrls 是数组格式,包含所有上传成功的图片路径
  945. const attachmentUrlsList = Array.isArray(formData.attachmentUrls)
  946. ? formData.attachmentUrls.filter(url => url && url.trim() !== "")
  947. : [];
  948. console.log("提交时的附件列表:", attachmentUrlsList);
  949. // 构建提交参数 - attachmentUrls 字段存储图片路径数组
  950. const params: any = {
  951. id: 0, // 新建时传0
  952. requirementTitle: formData.requirementTitle,
  953. renovationType: formData.renovationType,
  954. houseArea: formData.houseArea || 0,
  955. renovationBudget: formData.renovationBudget || 0,
  956. detailedRequirement: formData.detailedRequirement || "",
  957. expectedRenovationTime: formData.expectedRenovationTime,
  958. // 将上传获取的图片路径数组存入 attachmentUrls 字段,传递给保存接口
  959. attachmentUrls: attachmentUrlsList,
  960. contactName: formData.contactName,
  961. contactPhone: formData.contactPhone,
  962. city: formData.city,
  963. cityAdcode: formData.cityAdcode || "",
  964. detailedAddress: formData.detailedAddress || "",
  965. agreementConfirmed: formData.agreementConfirmed,
  966. storeId: formData.storeId || 0,
  967. auditStatus: 0,
  968. auditReason: "", // 拒绝原因字段,新建时为空
  969. status: 0,
  970. hasCommunicated: false,
  971. inquiryCount: 0,
  972. viewCount: 0,
  973. createdTime: "",
  974. updatedTime: "",
  975. storeAddress: "",
  976. storeAvatar: "",
  977. storeBlurb: "",
  978. storeName: "",
  979. storeTel: ""
  980. };
  981. console.log("提交参数:", params);
  982. const res: any = await saveOrUpdateDecoration(params);
  983. if (res.code == 200 || res.code == 0) {
  984. ElMessage.success("创建成功");
  985. // 返回到列表页面
  986. router.push("/storeDecorationManagement/decorationManagement");
  987. } else {
  988. ElMessage.error(res.msg || "创建失败");
  989. // 创建失败也返回到列表页面
  990. router.push("/storeDecorationManagement/decorationManagement");
  991. }
  992. } catch (error: any) {
  993. ElMessage.error(error?.msg || error?.message || "创建失败");
  994. // 创建失败也返回到列表页面
  995. router.push("/storeDecorationManagement/decorationManagement");
  996. } finally {
  997. submitLoading.value = false;
  998. }
  999. });
  1000. };
  1001. // 关闭对话框
  1002. const handleClose = () => {
  1003. dialogVisible.value = false;
  1004. // 返回到列表页面
  1005. router.push("/storeDecorationManagement/decorationManagement");
  1006. };
  1007. // 初始化
  1008. onMounted(() => {
  1009. getProvinceData();
  1010. });
  1011. </script>
  1012. <style lang="scss" scoped>
  1013. .add-container {
  1014. :deep(.el-dialog__body) {
  1015. max-height: 70vh;
  1016. padding: 20px;
  1017. overflow-y: auto;
  1018. }
  1019. :deep(.el-form-item) {
  1020. margin-bottom: 20px;
  1021. }
  1022. :deep(.el-radio-group) {
  1023. display: flex;
  1024. gap: 20px;
  1025. }
  1026. .upload-tip {
  1027. margin-top: 5px;
  1028. font-size: 12px;
  1029. color: #999999;
  1030. }
  1031. .dialog-footer {
  1032. padding: 20px 0 0;
  1033. text-align: center;
  1034. }
  1035. :deep(.el-upload--picture-card) {
  1036. width: 100px;
  1037. height: 100px;
  1038. }
  1039. :deep(.el-upload-list--picture-card .el-upload-list__item) {
  1040. width: 100px;
  1041. height: 100px;
  1042. overflow: hidden;
  1043. }
  1044. .attachment-upload-item {
  1045. position: relative;
  1046. width: 100%;
  1047. height: 100%;
  1048. overflow: hidden;
  1049. }
  1050. .attachment-upload-thumb {
  1051. display: block;
  1052. width: 100%;
  1053. height: 100%;
  1054. object-fit: cover;
  1055. background: #f0f2f5;
  1056. }
  1057. .attachment-upload-placeholder {
  1058. display: flex;
  1059. align-items: center;
  1060. justify-content: center;
  1061. width: 100%;
  1062. height: 100%;
  1063. color: #909399;
  1064. background: #f0f2f5;
  1065. .el-icon {
  1066. font-size: 28px;
  1067. }
  1068. }
  1069. .attachment-upload-handle {
  1070. position: absolute;
  1071. inset: 0;
  1072. display: flex;
  1073. gap: 8px;
  1074. align-items: center;
  1075. justify-content: center;
  1076. cursor: default;
  1077. background: rgb(0 0 0 / 55%);
  1078. opacity: 0;
  1079. transition: opacity 0.2s;
  1080. }
  1081. .attachment-upload-item:hover .attachment-upload-handle {
  1082. opacity: 1;
  1083. }
  1084. .attachment-upload-handle-icon {
  1085. display: flex;
  1086. flex-direction: column;
  1087. align-items: center;
  1088. justify-content: center;
  1089. padding: 0 6px;
  1090. color: #ffffff;
  1091. cursor: pointer;
  1092. .el-icon {
  1093. font-size: 18px;
  1094. }
  1095. span {
  1096. margin-top: 4px;
  1097. font-size: 12px;
  1098. line-height: 1;
  1099. }
  1100. }
  1101. .city-selector {
  1102. padding: 10px 0;
  1103. }
  1104. .cursor-pointer {
  1105. cursor: pointer;
  1106. }
  1107. .agreement-content {
  1108. height: 50vh;
  1109. padding: 10px;
  1110. overflow-y: auto;
  1111. font-size: 14px;
  1112. line-height: 1.6;
  1113. h3 {
  1114. margin-top: 20px;
  1115. margin-bottom: 10px;
  1116. font-weight: 600;
  1117. }
  1118. p {
  1119. margin-bottom: 10px;
  1120. }
  1121. ul {
  1122. margin-bottom: 10px;
  1123. margin-left: 20px;
  1124. li {
  1125. margin-bottom: 5px;
  1126. }
  1127. }
  1128. }
  1129. .dialog-footer-center {
  1130. display: flex;
  1131. align-items: center;
  1132. justify-content: center;
  1133. width: 100%;
  1134. .el-button {
  1135. width: 406px;
  1136. height: 60px;
  1137. color: #ffffff;
  1138. background-color: #6c8ff8;
  1139. border-radius: 10px;
  1140. }
  1141. }
  1142. }
  1143. </style>