component-patterns.mdc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. ---
  2. alwaysApply: false
  3. ---
  4. ### 组件开发模式
  5. #### 通用组件设计原则
  6. **组件职责单一**
  7. - 每个组件只负责一个明确的功能
  8. - 避免组件过于复杂,拆分成更小的子组件
  9. - 组件应该是可复用的,不依赖特定的业务逻辑
  10. **Props 设计规范**
  11. ```vue
  12. <!-- components/UserCard.vue -->
  13. <template>
  14. <view class="user-card" :class="{ 'user-card--disabled': disabled }">
  15. <image class="avatar" :src="avatar" mode="aspectFill" />
  16. <view class="info">
  17. <text class="name">{{ name }}</text>
  18. <text class="role">{{ role }}</text>
  19. </view>
  20. <view class="actions" v-if="showActions">
  21. <button @click="$emit('edit', user)">编辑</button>
  22. <button @click="$emit('delete', user)">删除</button>
  23. </view>
  24. </view>
  25. </template>
  26. <script>
  27. export default {
  28. name: 'UserCard',
  29. props: {
  30. // 用户数据对象
  31. user: {
  32. type: Object,
  33. required: true,
  34. default: () => ({})
  35. },
  36. // 用户头像
  37. avatar: {
  38. type: String,
  39. default: '/static/images/default-avatar.png'
  40. },
  41. // 用户名称
  42. name: {
  43. type: String,
  44. required: true
  45. },
  46. // 用户角色
  47. role: {
  48. type: String,
  49. default: ''
  50. },
  51. // 是否显示操作按钮
  52. showActions: {
  53. type: Boolean,
  54. default: true
  55. },
  56. // 是否禁用
  57. disabled: {
  58. type: Boolean,
  59. default: false
  60. }
  61. },
  62. emits: ['edit', 'delete'],
  63. computed: {
  64. // 组件内部计算属性
  65. displayName() {
  66. return this.name || '未知用户'
  67. }
  68. }
  69. }
  70. </script>
  71. ```
  72. #### 表单组件封装
  73. **通用表单项组件**
  74. ```vue
  75. <!-- components/FormItem.vue -->
  76. <template>
  77. <view class="form-item" :class="{ 'form-item--error': hasError }">
  78. <view class="form-item__label" v-if="label">
  79. <text class="required" v-if="required">*</text>
  80. {{ label }}
  81. </view>
  82. <view class="form-item__content">
  83. <slot />
  84. </view>
  85. <view class="form-item__error" v-if="hasError">
  86. {{ errorMessage }}
  87. </view>
  88. </view>
  89. </template>
  90. <script>
  91. export default {
  92. name: 'FormItem',
  93. props: {
  94. label: String,
  95. required: Boolean,
  96. error: String
  97. },
  98. computed: {
  99. hasError() {
  100. return !!this.error
  101. },
  102. errorMessage() {
  103. return this.error || ''
  104. }
  105. }
  106. }
  107. </script>
  108. <style lang="scss" scoped>
  109. .form-item {
  110. margin-bottom: 32rpx;
  111. &__label {
  112. font-size: 28rpx;
  113. color: #333;
  114. margin-bottom: 16rpx;
  115. .required {
  116. color: #ff4757;
  117. }
  118. }
  119. &__content {
  120. position: relative;
  121. }
  122. &__error {
  123. font-size: 24rpx;
  124. color: #ff4757;
  125. margin-top: 8rpx;
  126. }
  127. &--error {
  128. .form-item__content {
  129. border-color: #ff4757;
  130. }
  131. }
  132. }
  133. </style>
  134. ```
  135. **输入框组件**
  136. ```vue
  137. <!-- components/Input.vue -->
  138. <template>
  139. <view class="input-wrapper">
  140. <input
  141. class="input"
  142. :type="type"
  143. :value="modelValue"
  144. :placeholder="placeholder"
  145. :disabled="disabled"
  146. :maxlength="maxlength"
  147. @input="handleInput"
  148. @blur="handleBlur"
  149. @focus="handleFocus"
  150. />
  151. <view class="input__clear" v-if="clearable && modelValue" @click="handleClear">
  152. <text class="iconfont icon-close"></text>
  153. </view>
  154. </view>
  155. </template>
  156. <script>
  157. export default {
  158. name: 'Input',
  159. props: {
  160. modelValue: [String, Number],
  161. type: {
  162. type: String,
  163. default: 'text'
  164. },
  165. placeholder: String,
  166. disabled: Boolean,
  167. clearable: Boolean,
  168. maxlength: Number
  169. },
  170. emits: ['update:modelValue', 'focus', 'blur', 'clear'],
  171. methods: {
  172. handleInput(e) {
  173. this.$emit('update:modelValue', e.detail.value)
  174. },
  175. handleFocus(e) {
  176. this.$emit('focus', e)
  177. },
  178. handleBlur(e) {
  179. this.$emit('blur', e)
  180. },
  181. handleClear() {
  182. this.$emit('update:modelValue', '')
  183. this.$emit('clear')
  184. }
  185. }
  186. }
  187. </script>
  188. ```
  189. #### 列表组件模式
  190. **通用列表组件**
  191. ```vue
  192. <!-- components/List.vue -->
  193. <template>
  194. <view class="list">
  195. <!-- 加载状态 -->
  196. <view class="list__loading" v-if="loading">
  197. <text>加载中...</text>
  198. </view>
  199. <!-- 列表内容 -->
  200. <view class="list__content" v-else-if="list.length > 0">
  201. <view
  202. class="list__item"
  203. v-for="(item, index) in list"
  204. :key="getItemKey(item, index)"
  205. @click="$emit('item-click', item, index)"
  206. >
  207. <slot :item="item" :index="index">
  208. <!-- 默认列表项渲染 -->
  209. <view class="default-item">{{ item.title || item.name }}</view>
  210. </slot>
  211. </view>
  212. <!-- 加载更多 -->
  213. <view class="list__load-more" v-if="hasMore" @click="$emit('load-more')">
  214. <text v-if="loadingMore">加载中...</text>
  215. <text v-else>点击加载更多</text>
  216. </view>
  217. <!-- 没有更多 -->
  218. <view class="list__no-more" v-else>
  219. <text>没有更多数据了</text>
  220. </view>
  221. </view>
  222. <!-- 空状态 -->
  223. <view class="list__empty" v-else>
  224. <slot name="empty">
  225. <view class="empty-default">
  226. <image src="/static/images/empty.png" mode="aspectFit" />
  227. <text>暂无数据</text>
  228. </view>
  229. </slot>
  230. </view>
  231. <!-- 错误状态 -->
  232. <view class="list__error" v-if="error" @click="$emit('retry')">
  233. <text>{{ error }}</text>
  234. <text>点击重试</text>
  235. </view>
  236. </view>
  237. </template>
  238. <script>
  239. export default {
  240. name: 'List',
  241. props: {
  242. list: {
  243. type: Array,
  244. default: () => []
  245. },
  246. loading: Boolean,
  247. loadingMore: Boolean,
  248. hasMore: Boolean,
  249. error: String,
  250. itemKey: {
  251. type: String,
  252. default: 'id'
  253. }
  254. },
  255. emits: ['item-click', 'load-more', 'retry'],
  256. methods: {
  257. getItemKey(item, index) {
  258. return item[this.itemKey] || index
  259. }
  260. }
  261. }
  262. </script>
  263. ```
  264. #### 弹窗组件模式
  265. **通用弹窗组件**
  266. ```vue
  267. <!-- components/Modal.vue -->
  268. <template>
  269. <view class="modal" v-if="visible" @click="handleMaskClick">
  270. <view class="modal__content" @click.stop>
  271. <!-- 头部 -->
  272. <view class="modal__header" v-if="title || $slots.header">
  273. <slot name="header">
  274. <text class="modal__title">{{ title }}</text>
  275. <view class="modal__close" @click="handleClose">
  276. <text class="iconfont icon-close"></text>
  277. </view>
  278. </slot>
  279. </view>
  280. <!-- 内容 -->
  281. <view class="modal__body">
  282. <slot />
  283. </view>
  284. <!-- 底部 -->
  285. <view class="modal__footer" v-if="$slots.footer">
  286. <slot name="footer" />
  287. </view>
  288. </view>
  289. </view>
  290. </template>
  291. <script>
  292. export default {
  293. name: 'Modal',
  294. props: {
  295. visible: Boolean,
  296. title: String,
  297. maskClosable: {
  298. type: Boolean,
  299. default: true
  300. }
  301. },
  302. emits: ['update:visible', 'close'],
  303. methods: {
  304. handleClose() {
  305. this.$emit('update:visible', false)
  306. this.$emit('close')
  307. },
  308. handleMaskClick() {
  309. if (this.maskClosable) {
  310. this.handleClose()
  311. }
  312. }
  313. }
  314. }
  315. </script>
  316. <style lang="scss" scoped>
  317. .modal {
  318. position: fixed;
  319. top: 0;
  320. left: 0;
  321. right: 0;
  322. bottom: 0;
  323. background-color: rgba(0, 0, 0, 0.5);
  324. display: flex;
  325. align-items: center;
  326. justify-content: center;
  327. z-index: 1000;
  328. &__content {
  329. background-color: #fff;
  330. border-radius: 16rpx;
  331. min-width: 600rpx;
  332. max-width: 90%;
  333. max-height: 80%;
  334. overflow: hidden;
  335. }
  336. &__header {
  337. display: flex;
  338. align-items: center;
  339. justify-content: space-between;
  340. padding: 32rpx;
  341. border-bottom: 1px solid #eee;
  342. }
  343. &__title {
  344. font-size: 32rpx;
  345. font-weight: 500;
  346. color: #333;
  347. }
  348. &__close {
  349. width: 48rpx;
  350. height: 48rpx;
  351. display: flex;
  352. align-items: center;
  353. justify-content: center;
  354. color: #999;
  355. }
  356. &__body {
  357. padding: 32rpx;
  358. }
  359. &__footer {
  360. padding: 32rpx;
  361. border-top: 1px solid #eee;
  362. }
  363. }
  364. </style>
  365. ```
  366. #### 业务组件示例
  367. **订单卡片组件**
  368. ```vue
  369. <!-- components/OrderCard.vue -->
  370. <template>
  371. <view class="order-card" @click="$emit('click', order)">
  372. <view class="order-card__header">
  373. <text class="order-no">订单号:{{ order.orderNo }}</text>
  374. <text class="status" :class="`status--${order.status}`">
  375. {{ statusText }}
  376. </text>
  377. </view>
  378. <view class="order-card__content">
  379. <view class="products">
  380. <view
  381. class="product-item"
  382. v-for="product in order.products"
  383. :key="product.id"
  384. >
  385. <image :src="product.image" mode="aspectFill" />
  386. <view class="product-info">
  387. <text class="name">{{ product.name }}</text>
  388. <text class="spec">{{ product.spec }}</text>
  389. <text class="price">¥{{ product.price }} × {{ product.quantity }}</text>
  390. </view>
  391. </view>
  392. </view>
  393. </view>
  394. <view class="order-card__footer">
  395. <text class="total">合计:¥{{ order.totalAmount }}</text>
  396. <view class="actions">
  397. <button
  398. v-for="action in actions"
  399. :key="action.type"
  400. class="action-btn"
  401. :class="`action-btn--${action.type}`"
  402. @click.stop="$emit('action', action.type, order)"
  403. >
  404. {{ action.label }}
  405. </button>
  406. </view>
  407. </view>
  408. </view>
  409. </template>
  410. <script>
  411. export default {
  412. name: 'OrderCard',
  413. props: {
  414. order: {
  415. type: Object,
  416. required: true
  417. }
  418. },
  419. emits: ['click', 'action'],
  420. computed: {
  421. statusText() {
  422. const statusMap = {
  423. pending: '待付款',
  424. paid: '已付款',
  425. shipped: '已发货',
  426. delivered: '已送达',
  427. cancelled: '已取消'
  428. }
  429. return statusMap[this.order.status] || '未知状态'
  430. },
  431. actions() {
  432. const actionMap = {
  433. pending: [
  434. { type: 'pay', label: '去付款' },
  435. { type: 'cancel', label: '取消订单' }
  436. ],
  437. paid: [
  438. { type: 'remind', label: '提醒发货' }
  439. ],
  440. shipped: [
  441. { type: 'confirm', label: '确认收货' }
  442. ],
  443. delivered: [
  444. { type: 'review', label: '去评价' }
  445. ]
  446. }
  447. return actionMap[this.order.status] || []
  448. }
  449. }
  450. }
  451. </script>
  452. ```
  453. ### 组件开发模式
  454. #### 通用组件设计原则
  455. **组件职责单一**
  456. - 每个组件只负责一个明确的功能
  457. - 避免组件过于复杂,拆分成更小的子组件
  458. - 组件应该是可复用的,不依赖特定的业务逻辑
  459. **Props 设计规范**
  460. ```vue
  461. <!-- components/UserCard.vue -->
  462. <template>
  463. <view class="user-card" :class="{ 'user-card--disabled': disabled }">
  464. <image class="avatar" :src="avatar" mode="aspectFill" />
  465. <view class="info">
  466. <text class="name">{{ name }}</text>
  467. <text class="role">{{ role }}</text>
  468. </view>
  469. <view class="actions" v-if="showActions">
  470. <button @click="$emit('edit', user)">编辑</button>
  471. <button @click="$emit('delete', user)">删除</button>
  472. </view>
  473. </view>
  474. </template>
  475. <script>
  476. export default {
  477. name: 'UserCard',
  478. props: {
  479. // 用户数据对象
  480. user: {
  481. type: Object,
  482. required: true,
  483. default: () => ({})
  484. },
  485. // 用户头像
  486. avatar: {
  487. type: String,
  488. default: '/static/images/default-avatar.png'
  489. },
  490. // 用户名称
  491. name: {
  492. type: String,
  493. required: true
  494. },
  495. // 用户角色
  496. role: {
  497. type: String,
  498. default: ''
  499. },
  500. // 是否显示操作按钮
  501. showActions: {
  502. type: Boolean,
  503. default: true
  504. },
  505. // 是否禁用
  506. disabled: {
  507. type: Boolean,
  508. default: false
  509. }
  510. },
  511. emits: ['edit', 'delete'],
  512. computed: {
  513. // 组件内部计算属性
  514. displayName() {
  515. return this.name || '未知用户'
  516. }
  517. }
  518. }
  519. </script>
  520. ```
  521. #### 表单组件封装
  522. **通用表单项组件**
  523. ```vue
  524. <!-- components/FormItem.vue -->
  525. <template>
  526. <view class="form-item" :class="{ 'form-item--error': hasError }">
  527. <view class="form-item__label" v-if="label">
  528. <text class="required" v-if="required">*</text>
  529. {{ label }}
  530. </view>
  531. <view class="form-item__content">
  532. <slot />
  533. </view>
  534. <view class="form-item__error" v-if="hasError">
  535. {{ errorMessage }}
  536. </view>
  537. </view>
  538. </template>
  539. <script>
  540. export default {
  541. name: 'FormItem',
  542. props: {
  543. label: String,
  544. required: Boolean,
  545. error: String
  546. },
  547. computed: {
  548. hasError() {
  549. return !!this.error
  550. },
  551. errorMessage() {
  552. return this.error || ''
  553. }
  554. }
  555. }
  556. </script>
  557. <style lang="scss" scoped>
  558. .form-item {
  559. margin-bottom: 32rpx;
  560. &__label {
  561. font-size: 28rpx;
  562. color: #333;
  563. margin-bottom: 16rpx;
  564. .required {
  565. color: #ff4757;
  566. }
  567. }
  568. &__content {
  569. position: relative;
  570. }
  571. &__error {
  572. font-size: 24rpx;
  573. color: #ff4757;
  574. margin-top: 8rpx;
  575. }
  576. &--error {
  577. .form-item__content {
  578. border-color: #ff4757;
  579. }
  580. }
  581. }
  582. </style>
  583. ```
  584. **输入框组件**
  585. ```vue
  586. <!-- components/Input.vue -->
  587. <template>
  588. <view class="input-wrapper">
  589. <input
  590. class="input"
  591. :type="type"
  592. :value="modelValue"
  593. :placeholder="placeholder"
  594. :disabled="disabled"
  595. :maxlength="maxlength"
  596. @input="handleInput"
  597. @blur="handleBlur"
  598. @focus="handleFocus"
  599. />
  600. <view class="input__clear" v-if="clearable && modelValue" @click="handleClear">
  601. <text class="iconfont icon-close"></text>
  602. </view>
  603. </view>
  604. </template>
  605. <script>
  606. export default {
  607. name: 'Input',
  608. props: {
  609. modelValue: [String, Number],
  610. type: {
  611. type: String,
  612. default: 'text'
  613. },
  614. placeholder: String,
  615. disabled: Boolean,
  616. clearable: Boolean,
  617. maxlength: Number
  618. },
  619. emits: ['update:modelValue', 'focus', 'blur', 'clear'],
  620. methods: {
  621. handleInput(e) {
  622. this.$emit('update:modelValue', e.detail.value)
  623. },
  624. handleFocus(e) {
  625. this.$emit('focus', e)
  626. },
  627. handleBlur(e) {
  628. this.$emit('blur', e)
  629. },
  630. handleClear() {
  631. this.$emit('update:modelValue', '')
  632. this.$emit('clear')
  633. }
  634. }
  635. }
  636. </script>
  637. ```
  638. #### 列表组件模式
  639. **通用列表组件**
  640. ```vue
  641. <!-- components/List.vue -->
  642. <template>
  643. <view class="list">
  644. <!-- 加载状态 -->
  645. <view class="list__loading" v-if="loading">
  646. <text>加载中...</text>
  647. </view>
  648. <!-- 列表内容 -->
  649. <view class="list__content" v-else-if="list.length > 0">
  650. <view
  651. class="list__item"
  652. v-for="(item, index) in list"
  653. :key="getItemKey(item, index)"
  654. @click="$emit('item-click', item, index)"
  655. >
  656. <slot :item="item" :index="index">
  657. <!-- 默认列表项渲染 -->
  658. <view class="default-item">{{ item.title || item.name }}</view>
  659. </slot>
  660. </view>
  661. <!-- 加载更多 -->
  662. <view class="list__load-more" v-if="hasMore" @click="$emit('load-more')">
  663. <text v-if="loadingMore">加载中...</text>
  664. <text v-else>点击加载更多</text>
  665. </view>
  666. <!-- 没有更多 -->
  667. <view class="list__no-more" v-else>
  668. <text>没有更多数据了</text>
  669. </view>
  670. </view>
  671. <!-- 空状态 -->
  672. <view class="list__empty" v-else>
  673. <slot name="empty">
  674. <view class="empty-default">
  675. <image src="/static/images/empty.png" mode="aspectFit" />
  676. <text>暂无数据</text>
  677. </view>
  678. </slot>
  679. </view>
  680. <!-- 错误状态 -->
  681. <view class="list__error" v-if="error" @click="$emit('retry')">
  682. <text>{{ error }}</text>
  683. <text>点击重试</text>
  684. </view>
  685. </view>
  686. </template>
  687. <script>
  688. export default {
  689. name: 'List',
  690. props: {
  691. list: {
  692. type: Array,
  693. default: () => []
  694. },
  695. loading: Boolean,
  696. loadingMore: Boolean,
  697. hasMore: Boolean,
  698. error: String,
  699. itemKey: {
  700. type: String,
  701. default: 'id'
  702. }
  703. },
  704. emits: ['item-click', 'load-more', 'retry'],
  705. methods: {
  706. getItemKey(item, index) {
  707. return item[this.itemKey] || index
  708. }
  709. }
  710. }
  711. </script>
  712. ```
  713. #### 弹窗组件模式
  714. **通用弹窗组件**
  715. ```vue
  716. <!-- components/Modal.vue -->
  717. <template>
  718. <view class="modal" v-if="visible" @click="handleMaskClick">
  719. <view class="modal__content" @click.stop>
  720. <!-- 头部 -->
  721. <view class="modal__header" v-if="title || $slots.header">
  722. <slot name="header">
  723. <text class="modal__title">{{ title }}</text>
  724. <view class="modal__close" @click="handleClose">
  725. <text class="iconfont icon-close"></text>
  726. </view>
  727. </slot>
  728. </view>
  729. <!-- 内容 -->
  730. <view class="modal__body">
  731. <slot />
  732. </view>
  733. <!-- 底部 -->
  734. <view class="modal__footer" v-if="$slots.footer">
  735. <slot name="footer" />
  736. </view>
  737. </view>
  738. </view>
  739. </template>
  740. <script>
  741. export default {
  742. name: 'Modal',
  743. props: {
  744. visible: Boolean,
  745. title: String,
  746. maskClosable: {
  747. type: Boolean,
  748. default: true
  749. }
  750. },
  751. emits: ['update:visible', 'close'],
  752. methods: {
  753. handleClose() {
  754. this.$emit('update:visible', false)
  755. this.$emit('close')
  756. },
  757. handleMaskClick() {
  758. if (this.maskClosable) {
  759. this.handleClose()
  760. }
  761. }
  762. }
  763. }
  764. </script>
  765. <style lang="scss" scoped>
  766. .modal {
  767. position: fixed;
  768. top: 0;
  769. left: 0;
  770. right: 0;
  771. bottom: 0;
  772. background-color: rgba(0, 0, 0, 0.5);
  773. display: flex;
  774. align-items: center;
  775. justify-content: center;
  776. z-index: 1000;
  777. &__content {
  778. background-color: #fff;
  779. border-radius: 16rpx;
  780. min-width: 600rpx;
  781. max-width: 90%;
  782. max-height: 80%;
  783. overflow: hidden;
  784. }
  785. &__header {
  786. display: flex;
  787. align-items: center;
  788. justify-content: space-between;
  789. padding: 32rpx;
  790. border-bottom: 1px solid #eee;
  791. }
  792. &__title {
  793. font-size: 32rpx;
  794. font-weight: 500;
  795. color: #333;
  796. }
  797. &__close {
  798. width: 48rpx;
  799. height: 48rpx;
  800. display: flex;
  801. align-items: center;
  802. justify-content: center;
  803. color: #999;
  804. }
  805. &__body {
  806. padding: 32rpx;
  807. }
  808. &__footer {
  809. padding: 32rpx;
  810. border-top: 1px solid #eee;
  811. }
  812. }
  813. </style>
  814. ```
  815. #### 业务组件示例
  816. **订单卡片组件**
  817. ```vue
  818. <!-- components/OrderCard.vue -->
  819. <template>
  820. <view class="order-card" @click="$emit('click', order)">
  821. <view class="order-card__header">
  822. <text class="order-no">订单号:{{ order.orderNo }}</text>
  823. <text class="status" :class="`status--${order.status}`">
  824. {{ statusText }}
  825. </text>
  826. </view>
  827. <view class="order-card__content">
  828. <view class="products">
  829. <view
  830. class="product-item"
  831. v-for="product in order.products"
  832. :key="product.id"
  833. >
  834. <image :src="product.image" mode="aspectFill" />
  835. <view class="product-info">
  836. <text class="name">{{ product.name }}</text>
  837. <text class="spec">{{ product.spec }}</text>
  838. <text class="price">¥{{ product.price }} × {{ product.quantity }}</text>
  839. </view>
  840. </view>
  841. </view>
  842. </view>
  843. <view class="order-card__footer">
  844. <text class="total">合计:¥{{ order.totalAmount }}</text>
  845. <view class="actions">
  846. <button
  847. v-for="action in actions"
  848. :key="action.type"
  849. class="action-btn"
  850. :class="`action-btn--${action.type}`"
  851. @click.stop="$emit('action', action.type, order)"
  852. >
  853. {{ action.label }}
  854. </button>
  855. </view>
  856. </view>
  857. </view>
  858. </template>
  859. <script>
  860. export default {
  861. name: 'OrderCard',
  862. props: {
  863. order: {
  864. type: Object,
  865. required: true
  866. }
  867. },
  868. emits: ['click', 'action'],
  869. computed: {
  870. statusText() {
  871. const statusMap = {
  872. pending: '待付款',
  873. paid: '已付款',
  874. shipped: '已发货',
  875. delivered: '已送达',
  876. cancelled: '已取消'
  877. }
  878. return statusMap[this.order.status] || '未知状态'
  879. },
  880. actions() {
  881. const actionMap = {
  882. pending: [
  883. { type: 'pay', label: '去付款' },
  884. { type: 'cancel', label: '取消订单' }
  885. ],
  886. paid: [
  887. { type: 'remind', label: '提醒发货' }
  888. ],
  889. shipped: [
  890. { type: 'confirm', label: '确认收货' }
  891. ],
  892. delivered: [
  893. { type: 'review', label: '去评价' }
  894. ]
  895. }
  896. return actionMap[this.order.status] || []
  897. }
  898. }
  899. }
  900. </script>
  901. ```