ProductEdit.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. <template>
  2. <div class="product-edit admin-page">
  3. <el-card v-loading="pageLoading">
  4. <div slot="header" class="admin-page-header">
  5. <span class="admin-page-title">{{ isEdit ? '编辑商品' : '新增商品' }}</span>
  6. <div class="admin-page-actions">
  7. <el-button size="mini" @click="$router.back()">返回</el-button>
  8. </div>
  9. </div>
  10. <el-form ref="form" :model="form" :rules="rules" label-width="100px">
  11. <el-form-item label="商品名称" prop="name">
  12. <el-input v-model="form.name" placeholder="请输入商品名称" maxlength="100" />
  13. </el-form-item>
  14. <el-form-item label="商品类型" prop="productType">
  15. <el-select v-model="form.productType" placeholder="请选择商品类型" style="width: 200px;">
  16. <el-option label="实物商品" value="physical" />
  17. <el-option label="虚拟商品" value="virtual" />
  18. <el-option label="优惠券" value="coupon" />
  19. <el-option label="测评商品" value="assessment" />
  20. </el-select>
  21. </el-form-item>
  22. <!-- 测评商品扩展信息 -->
  23. <template v-if="form.productType === 'assessment'">
  24. <el-form-item label="测评类型">
  25. <el-select v-model="form.assessmentType" placeholder="请选择测评类型" style="width: 200px;">
  26. <el-option label="单包" value="single" />
  27. <el-option label="套餐" value="bundle" />
  28. </el-select>
  29. </el-form-item>
  30. <el-row :gutter="20">
  31. <el-col :span="8">
  32. <el-form-item label="测评次数">
  33. <el-input-number v-model="form.totalSessions" :min="1" :max="999" :precision="0" style="width: 100%;" controls />
  34. </el-form-item>
  35. </el-col>
  36. <el-col :span="8">
  37. <el-form-item label="有效期(天)">
  38. <el-input-number v-model="form.validityDays" :min="1" :max="3650" :precision="0" :default-value="365" style="width: 100%;" controls />
  39. </el-form-item>
  40. </el-col>
  41. <el-col :span="8">
  42. <el-form-item label="规划师范围">
  43. <el-select v-model="form.guideScope" placeholder="不限" style="width: 100%;">
  44. <el-option label="不限" value="all" />
  45. <el-option label="指定规划师" value="assigned" />
  46. <el-option label="自动分配" value="auto" />
  47. </el-select>
  48. </el-form-item>
  49. </el-col>
  50. </el-row>
  51. </template>
  52. <el-form-item label="商品简介" prop="intro">
  53. <el-input v-model="form.intro" type="textarea" :rows="2" placeholder="简短描述" maxlength="200" />
  54. </el-form-item>
  55. <el-form-item label="商品描述" prop="description">
  56. <div class="rich-editor">
  57. <Toolbar :editor="editorInstance" :defaultConfig="toolbarConfig" />
  58. <Editor :defaultConfig="editorConfig" v-model="form.description" @onCreated="handleEditorCreated" style="min-height: 300px;" />
  59. </div>
  60. </el-form-item>
  61. <el-form-item label="封面图片" prop="coverImage">
  62. <div class="cover-uploader">
  63. <el-upload
  64. :action="uploadActionUrl"
  65. :headers="uploadHeaders"
  66. :show-file-list="false"
  67. :on-success="handleCoverSuccess"
  68. :before-upload="beforeUpload"
  69. accept="image/*"
  70. >
  71. <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
  72. <i v-else class="el-icon-plus cover-icon"></i>
  73. </el-upload>
  74. <span class="cover-tip">点击上传封面图,建议尺寸 800x800</span>
  75. </div>
  76. </el-form-item>
  77. <el-form-item label="商品图片">
  78. <div class="images-uploader">
  79. <div v-for="(img, idx) in imageList" :key="idx" class="image-item">
  80. <img :src="img" class="image-preview" />
  81. <span class="image-remove" @click="removeImage(idx)">×</span>
  82. </div>
  83. <el-upload
  84. :action="uploadActionUrl"
  85. :headers="uploadHeaders"
  86. :show-file-list="false"
  87. :on-success="handleImageSuccess"
  88. :before-upload="beforeUpload"
  89. accept="image/*"
  90. >
  91. <i class="el-icon-plus image-add-icon"></i>
  92. </el-upload>
  93. </div>
  94. </el-form-item>
  95. <el-row :gutter="20">
  96. <el-col :span="8">
  97. <el-form-item label="售价(元)" prop="price">
  98. <el-input-number v-model="form.price" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
  99. </el-form-item>
  100. </el-col>
  101. <el-col :span="8">
  102. <el-form-item label="会员价(元)">
  103. <el-input-number v-model="form.memberPrice" :min="0" :precision="2" placeholder="0.00" style="width: 100%;" controls />
  104. </el-form-item>
  105. </el-col>
  106. <el-col :span="8">
  107. <el-form-item label="库存" prop="stock">
  108. <el-input-number v-model="form.stock" :min="0" :precision="0" placeholder="0" style="width: 100%;" controls />
  109. </el-form-item>
  110. </el-col>
  111. </el-row>
  112. <el-form-item label="供应商">
  113. <el-input v-model="form.vendorName" placeholder="供应商名称(管理员创建时填写)" />
  114. </el-form-item>
  115. <el-form-item label="所属体系">
  116. <el-select v-model="form.distributionSystemId" placeholder="不属于任何体系" clearable style="width: 100%" @change="handleSystemChange">
  117. <el-option v-for="s in supplySystems" :key="s.id" :label="s.name" :value="s.id"></el-option>
  118. </el-select>
  119. <span v-if="selectedSystem" style="font-size: 12px; color: #999; margin-left: 8px">
  120. 平台留利: {{ selectedSystem.platformProfitRate }}% → {{ selectedSystem.platformProfitRate * 10 }}‰
  121. </span>
  122. </el-form-item>
  123. <el-form-item label="配送方式">
  124. <el-radio-group v-model="form.deliveryMethod">
  125. <el-radio :label="1">快递</el-radio>
  126. <el-radio :label="2">自提</el-radio>
  127. <el-radio :label="3">两者皆可</el-radio>
  128. </el-radio-group>
  129. </el-form-item>
  130. <el-form-item label="会员专属">
  131. <el-radio-group v-model="form.memberEligible">
  132. <el-radio label="no">否</el-radio>
  133. <el-radio label="yes">是</el-radio>
  134. </el-radio-group>
  135. </el-form-item>
  136. <el-form-item label="商品维度">
  137. <dimension-weight-picker v-model="form.dimensionWeights" :show-presets="true" />
  138. </el-form-item>
  139. <el-form-item v-if="isEdit" label="SKU规格">
  140. <ProductSkuEditor :productId="form.id" />
  141. </el-form-item>
  142. <!-- 购买信息采集 -->
  143. <el-form-item label="购买信息采集">
  144. <div class="purchase-field-section">
  145. <el-checkbox v-model="purchaseFieldsEnabled" @change="handlePurchaseFieldsToggle">
  146. 启用购买信息采集(用户购买时需填写/确认以下信息)
  147. </el-checkbox>
  148. <div v-if="purchaseFieldsEnabled && predefinedFields.length" class="purchase-field-list">
  149. <div
  150. v-for="(pf, idx) in predefinedFields"
  151. :key="pf.fieldKey"
  152. class="purchase-field-item"
  153. >
  154. <el-checkbox
  155. v-model="pf.selected"
  156. @change="syncPurchaseFieldOrder"
  157. >
  158. {{ pf.fieldName }}
  159. </el-checkbox>
  160. <el-checkbox
  161. v-if="pf.selected"
  162. v-model="pf.required"
  163. class="purchase-field-required"
  164. size="mini"
  165. >
  166. 必填
  167. </el-checkbox>
  168. <span v-if="pf.selected" class="purchase-field-drag">
  169. <i class="el-icon-rank"></i>
  170. </span>
  171. </div>
  172. </div>
  173. </div>
  174. </el-form-item>
  175. <el-form-item>
  176. <el-button type="primary" :loading="saving" @click="handleSave">
  177. {{ isEdit ? '保存修改' : '创建商品' }}
  178. </el-button>
  179. <el-button @click="$router.back()">取消</el-button>
  180. </el-form-item>
  181. </el-form>
  182. </el-card>
  183. </div>
  184. </template>
  185. <script>
  186. import { getProduct, createProduct, updateProduct, listAllSupplySystems, getPredefinedPurchaseFields, getProductPurchaseFields, saveProductPurchaseFields } from '@/api/admin'
  187. import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight'
  188. import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
  189. import ProductSkuEditor from './components/ProductSkuEditor.vue'
  190. import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
  191. import '@wangeditor/editor/dist/css/style.css'
  192. export default {
  193. name: 'ProductEdit',
  194. components: { DimensionWeightPicker, ProductSkuEditor, Editor, Toolbar },
  195. data() {
  196. return {
  197. form: {
  198. name: '',
  199. intro: '',
  200. description: '',
  201. coverImage: '',
  202. images: '',
  203. productType: 'physical',
  204. price: null,
  205. memberPrice: null,
  206. stock: null,
  207. vendorName: '',
  208. vendorId: null,
  209. distributionSystemId: null,
  210. deliveryMethod: 1,
  211. memberEligible: 'no',
  212. dimensionWeights: [],
  213. // 测评商品扩展
  214. assessmentType: 'single',
  215. totalSessions: 1,
  216. validityDays: 365,
  217. guideScope: 'all'
  218. },
  219. imageList: [],
  220. supplySystems: [],
  221. rules: {
  222. name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
  223. productType: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
  224. price: [{ required: true, message: '请输入售价', trigger: 'blur' }],
  225. stock: [{ required: true, message: '请输入库存', trigger: 'blur' }]
  226. },
  227. saving: false,
  228. pageLoading: false,
  229. purchaseFieldsEnabled: false,
  230. predefinedFields: [],
  231. editorConfig: {
  232. MENU_CONF: {
  233. uploadImage: {
  234. server: '',
  235. headers: {},
  236. maxNumberOfFiles: 1,
  237. fieldName: 'file',
  238. customAlert: (msg, type) => {
  239. this.$message && this.$message({ message: msg, type: type || 'error' })
  240. },
  241. onSuccess(res, insertFn) {
  242. if (res.code === 200) {
  243. insertFn(res.data.url || res.data)
  244. }
  245. }
  246. }
  247. }
  248. },
  249. toolbarConfig: {},
  250. editorInstance: null
  251. }
  252. },
  253. computed: {
  254. isEdit() {
  255. return !!this.$route.query.id
  256. },
  257. uploadActionUrl() {
  258. const baseURL = process.env.VUE_APP_BASE_API || 'http://localhost:9082'
  259. return baseURL + '/api/admin/articles/upload/image'
  260. },
  261. uploadHeaders() {
  262. const token = localStorage.getItem('token')
  263. return { Authorization: 'Bearer ' + token }
  264. },
  265. selectedSystem() {
  266. if (!this.form.distributionSystemId || !this.supplySystems.length) return null
  267. return this.supplySystems.find(s => s.id === this.form.distributionSystemId)
  268. }
  269. },
  270. mounted() {
  271. this.editorConfig.MENU_CONF.uploadImage.server = this.uploadActionUrl
  272. this.editorConfig.MENU_CONF.uploadImage.headers = this.uploadHeaders
  273. this.loadSupplySystems()
  274. this.loadPredefinedFields()
  275. if (this.isEdit) {
  276. this.loadProduct(this.$route.query.id)
  277. }
  278. },
  279. methods: {
  280. handleEditorCreated(editor) {
  281. this.editorInstance = editor
  282. },
  283. async loadSupplySystems() {
  284. try {
  285. const res = await listAllSupplySystems()
  286. if (res.data) {
  287. this.supplySystems = res.data
  288. }
  289. } catch (e) {
  290. console.error(e)
  291. }
  292. },
  293. handleSystemChange(systemId) {
  294. if (!systemId) return
  295. const system = this.supplySystems.find(s => s.id === systemId)
  296. if (system && system.platformProfitRate != null) {
  297. this.form.profitRate = Math.round(system.platformProfitRate * 10)
  298. }
  299. },
  300. async loadProduct(id) {
  301. this.pageLoading = true
  302. try {
  303. const res = await getProduct(id)
  304. if (res.code === 200 && res.data) {
  305. const p = res.data
  306. this.form = {
  307. id: p.id,
  308. name: p.name || '',
  309. intro: p.intro || '',
  310. description: p.description || '',
  311. coverImage: p.coverImage || '',
  312. images: p.images || '',
  313. productType: p.productType || 'physical',
  314. price: (p.price != null) ? p.price / 100 : null,
  315. memberPrice: (p.memberPrice != null) ? p.memberPrice / 100 : null,
  316. stock: (p.stock != null) ? p.stock : null,
  317. vendorName: p.vendorName || '',
  318. vendorId: p.vendorId || null,
  319. distributionSystemId: p.distributionSystemId || null,
  320. deliveryMethod: p.deliveryMethod || 1,
  321. memberEligible: p.memberEligible || 'no',
  322. dimensionWeights: [],
  323. assessmentType: p.assessmentType || 'single',
  324. totalSessions: p.totalSessions || 1,
  325. validityDays: p.validityDays || 365,
  326. guideScope: p.guideScope || 'all'
  327. }
  328. if (p.images) {
  329. try {
  330. this.imageList = JSON.parse(p.images)
  331. } catch (e) {
  332. this.imageList = []
  333. }
  334. }
  335. // Load dimension weights
  336. this.loadDimensionWeights(p.id)
  337. // Load purchase fields config
  338. this.loadPurchaseFields(p.id)
  339. // v-model 已绑定 form.description,editor 内容自动更新
  340. }
  341. } catch (e) {
  342. this.$message.error('加载商品失败')
  343. } finally {
  344. this.pageLoading = false
  345. }
  346. },
  347. async loadDimensionWeights(productId) {
  348. try {
  349. const res = await getDimensionWeights('product', productId)
  350. if (res.code === 200 && res.data) {
  351. this.form.dimensionWeights = res.data.map(w => ({
  352. dimension: w.dimension,
  353. weight: w.weight,
  354. enabled: w.enabled
  355. }))
  356. }
  357. } catch (e) {
  358. // ignore
  359. }
  360. },
  361. async loadPredefinedFields() {
  362. try {
  363. const res = await getPredefinedPurchaseFields()
  364. if (res.code === 200 && Array.isArray(res.data)) {
  365. this.predefinedFields = res.data.map(f => ({
  366. fieldKey: f.fieldKey,
  367. fieldName: f.fieldName,
  368. selected: false,
  369. required: true
  370. }))
  371. }
  372. } catch (e) {
  373. console.warn('加载预定义字段失败', e)
  374. }
  375. },
  376. async loadPurchaseFields(productId) {
  377. try {
  378. const res = await getProductPurchaseFields(productId)
  379. if (res.code === 200 && Array.isArray(res.data)) {
  380. const configs = res.data
  381. this.purchaseFieldsEnabled = configs.length > 0
  382. this.predefinedFields.forEach(pf => {
  383. const match = configs.find(c => c.fieldKey === pf.fieldKey)
  384. pf.selected = !!match
  385. pf.required = match ? match.isRequired === 1 : false
  386. })
  387. }
  388. } catch (e) {
  389. console.warn('加载购买字段配置失败', e)
  390. }
  391. },
  392. handlePurchaseFieldsToggle(val) {
  393. if (!val) {
  394. this.predefinedFields.forEach(pf => { pf.selected = false })
  395. }
  396. },
  397. syncPurchaseFieldOrder() {
  398. // sortOrder is derived from selection order
  399. },
  400. buildPurchaseFieldPayload() {
  401. if (!this.purchaseFieldsEnabled) return []
  402. return this.predefinedFields
  403. .filter(pf => pf.selected)
  404. .map((pf, idx) => ({
  405. fieldKey: pf.fieldKey,
  406. fieldName: pf.fieldName,
  407. isRequired: pf.required ? 1 : 0,
  408. sortOrder: idx
  409. }))
  410. },
  411. beforeUpload(file) {
  412. const isImage = file.type.startsWith('image/')
  413. const isLt5M = file.size / 1024 / 1024 < 5
  414. if (!isImage) {
  415. this.$message.error('只能上传图片文件')
  416. return false
  417. }
  418. if (!isLt5M) {
  419. this.$message.error('图片大小不能超过 5MB')
  420. return false
  421. }
  422. return true
  423. },
  424. handleCoverSuccess(res) {
  425. if (res.code === 200 && res.data) {
  426. this.form.coverImage = res.data.url || res.data
  427. this.$message.success('封面上传成功')
  428. } else {
  429. this.$message.error(res.message || '上传失败')
  430. }
  431. },
  432. handleImageSuccess(res) {
  433. if (res.code === 200 && res.data) {
  434. const url = res.data.url || res.data
  435. this.imageList.push(url)
  436. this.form.images = JSON.stringify(this.imageList)
  437. this.$message.success('图片上传成功')
  438. } else {
  439. this.$message.error(res.message || '上传失败')
  440. }
  441. },
  442. removeImage(idx) {
  443. this.imageList.splice(idx, 1)
  444. this.form.images = JSON.stringify(this.imageList)
  445. },
  446. async handleSave() {
  447. this.$refs.form.validate(async (valid) => {
  448. if (!valid) return
  449. this.saving = true
  450. try {
  451. const productData = {
  452. ...this.form,
  453. price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
  454. memberPrice: (this.form.memberPrice != null) ? Math.round(this.form.memberPrice * 100) : null,
  455. images: JSON.stringify(this.imageList)
  456. }
  457. // Extract dimensionWeights before sending to product API
  458. const dimensionWeights = productData.dimensionWeights
  459. delete productData.dimensionWeights
  460. // Build ProductCreateRequest payload
  461. const requestPayload = { product: productData }
  462. if (productData.productType === 'assessment') {
  463. requestPayload.assessmentExt = {
  464. assessmentType: productData.assessmentType,
  465. totalSessions: productData.totalSessions,
  466. validityDays: productData.validityDays,
  467. guideScope: productData.guideScope
  468. }
  469. }
  470. // Remove assessment ext fields from product object
  471. delete productData.assessmentType
  472. delete productData.totalSessions
  473. delete productData.validityDays
  474. delete productData.guideScope
  475. let res
  476. let productId = this.form.id
  477. if (this.isEdit) {
  478. res = await updateProduct(requestPayload)
  479. productId = this.form.id
  480. } else {
  481. res = await createProduct(requestPayload)
  482. // Get the created product id
  483. if (res.code === 200 && res.data) {
  484. productId = res.data.id || res.data
  485. }
  486. }
  487. if (res.code === 200) {
  488. // Save dimension weights
  489. if (productId && Array.isArray(dimensionWeights)) {
  490. try {
  491. await saveDimensionWeights('product', productId, dimensionWeights)
  492. } catch (e) {
  493. console.warn('保存维度权重失败', e)
  494. }
  495. }
  496. // Save purchase fields config
  497. if (productId) {
  498. try {
  499. const fields = this.buildPurchaseFieldPayload()
  500. await saveProductPurchaseFields(productId, fields)
  501. } catch (e) {
  502. console.warn('保存购买字段配置失败', e)
  503. }
  504. }
  505. this.$message.success(this.isEdit ? '保存成功' : '创建成功')
  506. this.$router.push('/product-manage')
  507. } else {
  508. this.$message.error(res.message || '操作失败')
  509. }
  510. } catch (e) {
  511. this.$message.error('操作失败')
  512. } finally {
  513. this.saving = false
  514. }
  515. })
  516. }
  517. }
  518. }
  519. </script>
  520. <style scoped>
  521. .product-edit {
  522. padding: 20px;
  523. }
  524. .cover-uploader {
  525. display: flex;
  526. align-items: center;
  527. gap: 12px;
  528. }
  529. .cover-preview {
  530. width: 100px;
  531. height: 100px;
  532. object-fit: cover;
  533. border-radius: 4px;
  534. border: 1px solid #ddd;
  535. }
  536. .cover-icon {
  537. width: 100px;
  538. height: 100px;
  539. line-height: 100px;
  540. text-align: center;
  541. font-size: 28px;
  542. color: #8c939d;
  543. border: 1px dashed #d9d9d9;
  544. border-radius: 4px;
  545. display: block;
  546. cursor: pointer;
  547. }
  548. .cover-icon:hover {
  549. border-color: #409eff;
  550. color: #409eff;
  551. }
  552. .cover-tip {
  553. font-size: 12px;
  554. color: #999;
  555. }
  556. .images-uploader {
  557. display: flex;
  558. flex-wrap: wrap;
  559. gap: 8px;
  560. }
  561. .image-item {
  562. position: relative;
  563. width: 80px;
  564. height: 80px;
  565. }
  566. .image-preview {
  567. width: 80px;
  568. height: 80px;
  569. object-fit: cover;
  570. border-radius: 4px;
  571. border: 1px solid #ddd;
  572. }
  573. .image-remove {
  574. position: absolute;
  575. top: -6px;
  576. right: -6px;
  577. width: 18px;
  578. height: 18px;
  579. background: #f56c6c;
  580. color: #fff;
  581. border-radius: 50%;
  582. text-align: center;
  583. line-height: 18px;
  584. font-size: 12px;
  585. cursor: pointer;
  586. }
  587. .image-add-icon {
  588. width: 80px;
  589. height: 80px;
  590. line-height: 80px;
  591. text-align: center;
  592. font-size: 24px;
  593. color: #8c939d;
  594. border: 1px dashed #d9d9d9;
  595. border-radius: 4px;
  596. display: block;
  597. cursor: pointer;
  598. }
  599. .image-add-icon:hover {
  600. border-color: #409eff;
  601. color: #409eff;
  602. }
  603. .product-edit .el-card {
  604. width: 100%;
  605. }
  606. .product-edit .el-form {
  607. width: 100%;
  608. }
  609. .rich-editor {
  610. border: 1px solid #dcdfe6;
  611. border-radius: 4px;
  612. }
  613. .rich-editor /deep/ .w-e-toolbar {
  614. flex-wrap: wrap;
  615. }
  616. .rich-editor /deep/ .w-e-text-container {
  617. min-height: 300px;
  618. }
  619. .purchase-field-section {
  620. display: flex;
  621. flex-direction: column;
  622. gap: 12px;
  623. }
  624. .purchase-field-list {
  625. display: flex;
  626. flex-wrap: wrap;
  627. gap: 8px;
  628. padding: 12px;
  629. background: #fafafa;
  630. border-radius: 4px;
  631. border: 1px solid #eee;
  632. }
  633. .purchase-field-item {
  634. display: flex;
  635. align-items: center;
  636. gap: 8px;
  637. padding: 6px 12px;
  638. background: #fff;
  639. border: 1px solid #e8e8e8;
  640. border-radius: 4px;
  641. }
  642. .purchase-field-required {
  643. font-size: 12px;
  644. }
  645. .purchase-field-drag {
  646. color: #ccc;
  647. cursor: move;
  648. }
  649. </style>