EstablishOrganization.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <script setup lang="ts">
  2. import { inject, onMounted, ref } from 'vue';
  3. import { ColorPicker } from 'vue-color-kit';
  4. import { message } from 'ant-design-vue';
  5. import SvgIcon from '@/components/SvgIcon.vue';
  6. import { useDictData } from '@/hooks/dict-data';
  7. import { useRequest } from '@/hooks/request';
  8. import { t } from '@/i18n';
  9. import { addOrganization, addUploadLogo, getInfoListByOrgId, groupList, updateOrganization } from '@/api';
  10. import { DictCode } from '@/constants';
  11. import { SET_COLOR_PRIMARY } from '@/constants/inject-key';
  12. import EquipmentLimitations from './EquipmentLimitations.vue';
  13. import type {
  14. CreateCustomer,
  15. EquipmentLimitationsItem,
  16. EquipmentTypeItem,
  17. OrgDeviceLimit,
  18. UseGuideStepItemExpose,
  19. UseGuideStepItemProps,
  20. } from '@/types';
  21. import 'vue-color-kit/dist/vue-color-kit.css';
  22. // 引入样式文件
  23. interface ColorStyle {
  24. background: string;
  25. border: string;
  26. index: number;
  27. color: string;
  28. }
  29. interface NewColor {
  30. hex: string;
  31. }
  32. const { dictData: dataValidityPeriod, getDictData: getDataValidityPeriod } = useDictData(DictCode.DataValidityPeriod);
  33. const equipmentLimitationsRefs = ref<InstanceType<typeof EquipmentLimitations>[]>([]);
  34. const equipmentLimitationsList = ref<EquipmentLimitationsItem[]>([
  35. {
  36. deviceGlobalId: undefined,
  37. upperLimit: 0,
  38. },
  39. ]);
  40. const orgDeviceLimits = ref<OrgDeviceLimit[]>([]);
  41. const setColorPrimary = inject(SET_COLOR_PRIMARY, undefined);
  42. const fileList = ref([]);
  43. const fileImg = ref<Blob>();
  44. const customizationColor = ref<boolean>(false);
  45. const { handleRequest } = useRequest();
  46. const equipmentType = ref<EquipmentTypeItem[]>([]);
  47. const props = defineProps<UseGuideStepItemProps<CreateCustomer>>();
  48. const colorStyle: ColorStyle[] = [
  49. {
  50. background: 'background:#32bac0',
  51. border: 'border:1px solid #32bac0',
  52. color: '#32bac0',
  53. index: 0,
  54. },
  55. {
  56. background: 'background:#256AFE',
  57. border: 'border:1px solid #256AFE',
  58. color: '#256AFE',
  59. index: 1,
  60. },
  61. {
  62. background: 'background:#00A94D',
  63. border: 'border:1px solid #00A94D',
  64. color: '#00A94D',
  65. index: 2,
  66. },
  67. {
  68. background: 'background:#7866FF',
  69. border: 'border:1px solid #7866FF',
  70. color: '#7866FF',
  71. index: 3,
  72. },
  73. ];
  74. // 预定义颜色组
  75. const presetColors = ref<string[]>([
  76. '#000000',
  77. '#FFFFFF',
  78. '#FF1900',
  79. '#F47365',
  80. '#FFB243',
  81. '#FFE623',
  82. '#6EFF2A',
  83. '#1BC7B1',
  84. '#00BEFF',
  85. '#2E81FF',
  86. '#5D61FF',
  87. '#FF89CF',
  88. '#FC3CAD',
  89. '#BF3DCE',
  90. '#8E00A7',
  91. ]);
  92. // 颜色变化回调函数
  93. const handleColorChange = (newColor: NewColor) => {
  94. props.form.selectedColor = newColor.hex;
  95. setColorPrimary?.(props.form.selectedColor);
  96. props.form.themeColor = props.form.selectedColor;
  97. };
  98. const addCustomizationColor = () => {
  99. customizationColor.value = !customizationColor.value;
  100. };
  101. const colorClick = (color: string) => {
  102. setColorPrimary?.(color);
  103. props.form.themeColor = color;
  104. };
  105. const finish = async () => {
  106. orgDeviceLimits.value = [];
  107. // eslint-disable-next-line no-useless-catch
  108. try {
  109. // 创建校验任务队列(包含所有子组件+父组件自身)
  110. const allTasks = [...equipmentLimitationsRefs.value.map((c) => c.formRefSubmit())];
  111. // 并行执行所有校验(父+子)
  112. await Promise.all(allTasks);
  113. equipmentLimitationsList.value.forEach((item) => {
  114. orgDeviceLimits.value.push({
  115. deviceGlobalId: item.deviceGlobalId,
  116. upperLimit: item.upperLimit,
  117. });
  118. });
  119. orgDeviceLimits.value.push({
  120. deviceGlobalId: -1,
  121. upperLimit: props.form.stationsNumber,
  122. });
  123. if (props.form.id) {
  124. updateOrganization({
  125. orgName: props.form.orgName,
  126. logo: props.form.logo,
  127. themeColor: props.form.themeColor,
  128. dataValidityPeriod: props.form.dataValidityPeriod,
  129. enabled: '1',
  130. id: props.form.id,
  131. orgDeviceLimits: orgDeviceLimits.value,
  132. startTenancy: props.form.leaseTerm[0].format('YYYY-MM-DD'),
  133. endTenancy: props.form.leaseTerm[1].format('YYYY-MM-DD'),
  134. });
  135. } else {
  136. props.form.id = await addOrganization({
  137. orgName: props.form.orgName,
  138. logo: props.form.logo,
  139. themeColor: props.form.themeColor,
  140. dataValidityPeriod: props.form.dataValidityPeriod,
  141. enabled: '1',
  142. orgDeviceLimits: orgDeviceLimits.value,
  143. startTenancy: props.form.leaseTerm[0].format('YYYY-MM-DD'),
  144. endTenancy: props.form.leaseTerm[1].format('YYYY-MM-DD'),
  145. });
  146. }
  147. } catch (err) {
  148. throw err;
  149. }
  150. };
  151. const beforeUpload = (file: Blob) => {
  152. fileImg.value = file;
  153. const isJpgOrPng = file.type === 'image/jpg' || file.type === 'image/png' || file.type === 'image/jpeg';
  154. console.log(isJpgOrPng);
  155. if (!isJpgOrPng) {
  156. message.error('请上传JPG/PNG/JPEG格式的图片!');
  157. return false;
  158. }
  159. };
  160. const addClick = () => {
  161. equipmentLimitationsList.value.push({
  162. deviceGlobalId: undefined,
  163. upperLimit: 0,
  164. });
  165. };
  166. const deleteClick = (index: number) => {
  167. equipmentLimitationsList.value.splice(index, 1);
  168. };
  169. const handleChange = () => {
  170. if (fileImg.value) {
  171. // 上传接口
  172. handleRequest(async () => {
  173. const { fileName } = await addUploadLogo(fileImg.value as Blob);
  174. props.form.imageUrl = URL.createObjectURL(fileImg.value as Blob);
  175. props.form.logo = fileName;
  176. });
  177. }
  178. };
  179. defineExpose<UseGuideStepItemExpose>({
  180. finish,
  181. });
  182. onMounted(() => {
  183. handleRequest(async () => {
  184. await getDataValidityPeriod();
  185. equipmentType.value = await groupList({
  186. dataType: 1,
  187. });
  188. if (props.form.id) {
  189. equipmentLimitationsList.value = [];
  190. const data = await getInfoListByOrgId(props.form.id);
  191. data.forEach((item) => {
  192. if (item.deviceGlobalId !== -1) {
  193. equipmentLimitationsList.value.push({
  194. deviceGlobalId: item.deviceGlobalId,
  195. upperLimit: item.upperLimit,
  196. });
  197. }
  198. });
  199. }
  200. });
  201. });
  202. </script>
  203. <template>
  204. <div class="organization text">
  205. <AFormItem label="组织名称" name="orgName">
  206. <AInput v-model:value="form.orgName" class="organization-input" :placeholder="$t('common.pleaseEnter')" />
  207. </AFormItem>
  208. <AFormItem label="租期" name="leaseTerm">
  209. <ARangePicker
  210. class="organization-input"
  211. v-model:value="form.leaseTerm"
  212. :allow-clear="false"
  213. :separator="$t('common.to')"
  214. />
  215. </AFormItem>
  216. <div class="upper-limit">设置数量上限</div>
  217. <AFormItem label="站房数(设备组)" name="stationsNumber">
  218. <AInputNumber
  219. :placeholder="t('common.pleaseEnter')"
  220. v-model:value="form.stationsNumber"
  221. class="organization-input"
  222. addon-after="个"
  223. />
  224. </AFormItem>
  225. <div v-for="(item, index) in equipmentLimitationsList" :key="index">
  226. <EquipmentLimitations
  227. ref="equipmentLimitationsRefs"
  228. :index="index"
  229. :form="item"
  230. :equipment-type="equipmentType"
  231. @addClick="addClick"
  232. @deleteClick="deleteClick"
  233. />
  234. </div>
  235. <AFormItem label="数据存储时长" name="dataValidityPeriod">
  236. <ASelect
  237. class="organization-input"
  238. v-model:value="form.dataValidityPeriod"
  239. :options="dataValidityPeriod"
  240. :field-names="{ label: 'dictValue', value: 'dictEngValue' }"
  241. :placeholder="$t('common.plzSelect')"
  242. />
  243. </AFormItem>
  244. <AFormItem label="Logo">
  245. <div class="upload-dev">
  246. <AUpload
  247. v-model:file-list="fileList"
  248. name="file"
  249. list-type="picture-card"
  250. class="avatar-uploader"
  251. action=""
  252. :show-upload-list="false"
  253. :before-upload="beforeUpload"
  254. :custom-request="handleChange"
  255. >
  256. <template v-if="form.imageUrl">
  257. <div class="img-dev">
  258. <img class="img-style" :src="form.imageUrl" alt="avatar" />
  259. <div class="background-style"></div>
  260. </div>
  261. </template>
  262. <div v-else>
  263. <SvgIcon class="icon-size" name="upload-pictures" />
  264. <div class="text">上传图片</div>
  265. </div>
  266. </AUpload>
  267. <div class="format-text">支持JPG/PNG/JPEG格式的图片</div>
  268. </div>
  269. </AFormItem>
  270. <AFlex>
  271. <AFlex align="center">
  272. <div class="text">主题色</div>
  273. <AFlex class="color-list">
  274. <div v-for="(item, index) in colorStyle" :key="index">
  275. <div @click="colorClick(item.color)">
  276. <AFlex
  277. justify="center"
  278. align="center"
  279. :style="form.themeColor === item.color ? item.border : 'border:1px solid #fff;'"
  280. class="color-div"
  281. >
  282. <div :style="item.background" class="color-style"></div>
  283. </AFlex>
  284. </div>
  285. </div>
  286. <AFlex class="flex-relative">
  287. <div @click="addCustomizationColor">
  288. <AFlex class="color-customization" justify="space-between" align="center">
  289. <div class="customization-style" :style="'background:' + form.selectedColor"></div>
  290. <div class="customization-text">{{ form.selectedColor }}</div>
  291. </AFlex>
  292. </div>
  293. <div class="color-selection">
  294. <ColorPicker
  295. v-show="customizationColor"
  296. v-model:color="form.selectedColor"
  297. :colors-default="presetColors"
  298. theme="dark"
  299. @changeColor="handleColorChange"
  300. style="width: 220px"
  301. class="picker-color"
  302. />
  303. </div>
  304. </AFlex>
  305. </AFlex>
  306. </AFlex>
  307. </AFlex>
  308. <div class="display-effect">
  309. <div class="display-effect-top">
  310. <span class="desktop-header-dot" v-for="i in 3" :key="i"></span>
  311. </div>
  312. <AFlex class="desktop-content" wrap="wrap" justify="space-between">
  313. <div class="desktop-content-div" v-for="i in 6" :key="i">
  314. <AFlex>
  315. <div class="desktop-content-top-left"></div>
  316. <AFlex :vertical="true">
  317. <div class="desktop-content-top-right"></div>
  318. <div class="desktop-content-top-right-div"></div>
  319. </AFlex>
  320. </AFlex>
  321. <div class="desktop-content-bottom"></div>
  322. <div class="desktop-content-bottom-div"></div>
  323. </div>
  324. </AFlex>
  325. </div>
  326. </div>
  327. </template>
  328. <style lang="scss" scoped>
  329. .upper-limit {
  330. margin-top: 40px;
  331. margin-bottom: 16px;
  332. font-size: 16px;
  333. font-style: normal;
  334. font-weight: 500;
  335. line-height: 24px;
  336. color: #333;
  337. text-align: left;
  338. }
  339. .background-style {
  340. position: absolute;
  341. top: 0;
  342. left: 0;
  343. z-index: 99999;
  344. display: flex;
  345. gap: 20px;
  346. align-items: center;
  347. justify-content: center;
  348. width: 100%;
  349. height: 100%;
  350. background: rgb(0 0 0 / 50%);
  351. opacity: 0;
  352. transition: opacity 0.3s;
  353. }
  354. .img-dev {
  355. position: relative;
  356. width: 100%;
  357. height: 100%;
  358. }
  359. .img-style {
  360. width: 100%;
  361. height: 100%;
  362. object-fit: cover;
  363. }
  364. .color-selection {
  365. position: absolute;
  366. top: -270px;
  367. left: 80px;
  368. width: 100%;
  369. height: 100%;
  370. }
  371. .picker-color {
  372. :deep(.color-alpha) {
  373. display: none !important;
  374. }
  375. :deep(.color-type) {
  376. display: none !important;
  377. }
  378. :deep(.hue) {
  379. margin-left: 20px;
  380. }
  381. }
  382. .customization-text {
  383. font-size: 12px;
  384. font-style: normal;
  385. font-weight: 400;
  386. line-height: 20px;
  387. color: #999;
  388. text-align: left;
  389. }
  390. .customization-style {
  391. width: 24px;
  392. height: 24px;
  393. border-radius: 4px;
  394. }
  395. .flex-relative {
  396. position: relative;
  397. }
  398. .color-customization {
  399. width: 90px;
  400. height: 32px;
  401. padding: 4px;
  402. cursor: pointer;
  403. border: 1px solid #d6d6d6;
  404. border-radius: 4px;
  405. }
  406. .desktop-content {
  407. position: relative;
  408. top: -30px;
  409. left: 16px;
  410. width: 288px;
  411. }
  412. .desktop-content-bottom-div {
  413. width: 48px;
  414. height: 3px;
  415. background: #d8d8d8;
  416. border-radius: 2px;
  417. }
  418. .desktop-content-bottom {
  419. width: 71px;
  420. height: 3px;
  421. margin: 6px 0;
  422. background: #d8d8d8;
  423. border-radius: 2px;
  424. }
  425. .desktop-content-top-right-div {
  426. width: 26px;
  427. height: 3px;
  428. background: #d8d8d8;
  429. border-radius: 2px;
  430. }
  431. .desktop-content-top-right {
  432. width: 40px;
  433. height: 3px;
  434. margin: 6px 0;
  435. background: #d8d8d8;
  436. border-radius: 2px;
  437. }
  438. .desktop-content-top-left {
  439. width: 24px;
  440. height: 24px;
  441. margin-right: 8px;
  442. background: var(--antd-color-primary-opacity-50);
  443. border: 1px solid var(--antd-color-primary);
  444. border-radius: 4px;
  445. }
  446. .desktop-content-div {
  447. width: 88px;
  448. height: 57px;
  449. padding: 6px;
  450. margin-bottom: 12px;
  451. background: #fff;
  452. border: 1px solid #d9dbe2;
  453. border-radius: 4px;
  454. }
  455. .display-effect-top {
  456. display: flex;
  457. width: 319px;
  458. height: 94px;
  459. padding-top: 9px;
  460. padding-left: 16px;
  461. background-color: var(--antd-color-primary);
  462. border-top-left-radius: 8px;
  463. border-top-right-radius: 8px;
  464. }
  465. .display-effect {
  466. width: 320px;
  467. height: 206px;
  468. margin-top: 24px;
  469. margin-left: 138px;
  470. background: #fafafa;
  471. border: 1px solid var(--antd-color-primary);
  472. border-radius: 8px;
  473. }
  474. .desktop-header-dot {
  475. width: 6px;
  476. height: 6px;
  477. background-color: white;
  478. border-radius: 50%;
  479. & + & {
  480. margin-left: 6px;
  481. }
  482. }
  483. .color-list {
  484. margin-left: 95px;
  485. }
  486. .color-style {
  487. width: 24px;
  488. height: 24px;
  489. border-radius: 4px;
  490. }
  491. .color-div {
  492. width: 32px;
  493. height: 32px;
  494. margin-right: 4px;
  495. cursor: pointer;
  496. border-radius: 6px;
  497. }
  498. .upload-dev {
  499. margin-top: 8px;
  500. margin-left: 10px;
  501. }
  502. .icon-size {
  503. font-size: 24px;
  504. }
  505. .format-text {
  506. font-size: 12px;
  507. font-style: normal;
  508. font-weight: 400;
  509. line-height: 20px;
  510. color: #999;
  511. text-align: left;
  512. }
  513. .organization-input {
  514. width: 256px;
  515. height: 32px;
  516. margin-left: 10px;
  517. }
  518. .text {
  519. font-size: 14px;
  520. font-style: normal;
  521. font-weight: 400;
  522. line-height: 22px;
  523. color: rgb(0 0 0 / 65%);
  524. text-align: left;
  525. }
  526. :deep(.organization-input) {
  527. .ant-input-number-group .ant-input-number-group-addon {
  528. background-color: #fff;
  529. }
  530. }
  531. </style>