PackageTemplates.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <template>
  2. <div class="package-templates admin-page">
  3. <el-card>
  4. <div slot="header">
  5. <span>套餐模板管理</span>
  6. <el-input v-model="keyword" placeholder="搜索套餐..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
  7. <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 添加模板</el-button>
  8. <div style="overflow:auto;max-height:calc(100vh - 300px);">
  9. <el-table style="max-height:calc(100vh - 300px);" :data="filteredData" stripe>
  10. <el-table-column prop="id" label="ID" width="80"></el-table-column>
  11. <el-table-column prop="name" label="模板名称"></el-table-column>
  12. <el-table-column prop="validityMonths" label="有效期(月)" width="100"></el-table-column>
  13. <el-table-column prop="referencePrice" label="参考价格" width="100">
  14. <template slot-scope="scope">{{ formatPriceWithSymbol(scope.row.referencePrice) }}</template>
  15. </el-table-column>
  16. <el-table-column prop="minPrice" label="最低售价" width="100">
  17. <template slot-scope="scope">{{ formatPriceWithSymbol(scope.row.minPrice) }}</template>
  18. </el-table-column>
  19. <el-table-column prop="description" label="描述"></el-table-column>
  20. <el-table-column prop="status" label="状态" width="100">
  21. <template slot-scope="scope">
  22. <el-tag :type="scope.row.status === 'ACTIVE' ? 'success' : 'danger'">
  23. {{ scope.row.status === 'ACTIVE' ? '启用' : '停用' }}
  24. </el-tag>
  25. </template>
  26. </el-table-column>
  27. <el-table-column label="操作" width="200" fixed="right">
  28. <template slot-scope="{ row }">
  29. <el-button size="mini" type="primary" @click="handleEdit(row)">编辑</el-button>
  30. <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
  31. <el-button size="mini">
  32. 更多<i class="el-icon-arrow-down el-icon--right"></i>
  33. </el-button>
  34. <el-dropdown-menu slot="dropdown">
  35. <el-dropdown-item command="viewServices" icon="el-icon-document">服务项</el-dropdown-item>
  36. <el-dropdown-item command="toggleStatus" :icon="row.status === 'ACTIVE' ? 'el-icon-close' : 'el-icon-check'">{{ row.status === 'ACTIVE' ? '停用' : '启用' }}</el-dropdown-item>
  37. </el-dropdown-menu>
  38. </el-dropdown>
  39. </template>
  40. </el-table-column>
  41. </el-table></div>
  42. </el-card>
  43. <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑套餐模板' : '添加套餐模板'" width="600px">
  44. <el-form :model="form" label-width="120px">
  45. <el-form-item label="模板名称">
  46. <el-input v-model="form.name"></el-input>
  47. </el-form-item>
  48. <el-form-item label="有效期(月)">
  49. <el-input-number v-model="form.validityMonths" :min="1"></el-input-number>
  50. </el-form-item>
  51. <el-form-item label="参考价格(元)">
  52. <el-input-number v-model="form.referencePrice" :min="0" :precision="2"></el-input-number>
  53. </el-form-item>
  54. <el-form-item label="最低售价(元)">
  55. <el-input-number v-model="form.minPrice" :min="0" :precision="2"></el-input-number>
  56. <div class="form-tip">成长规划师定价不能低于此价格</div>
  57. </el-form-item>
  58. <el-form-item label="描述">
  59. <el-input type="textarea" v-model="form.description" :rows="3"></el-input>
  60. </el-form-item>
  61. </el-form>
  62. <div slot="footer">
  63. <el-button @click="dialogVisible = false">取消</el-button>
  64. <el-button type="primary" @click="handleSubmit">保存</el-button>
  65. </div>
  66. </el-dialog>
  67. <el-dialog :visible.sync="servicesVisible" title="套餐服务项" width="600px">
  68. <div class="services-list">
  69. <div v-if="currentServices.length === 0" class="empty-tip">暂无服务项</div>
  70. <div class="service-item" v-for="(service, index) in currentServices" :key="index">
  71. <div class="service-info">
  72. <span class="service-name">{{ service.serviceName }}</span>
  73. <span class="service-qty">{{ service.quantity }} {{ service.unit }}</span>
  74. </div>
  75. <span class="service-price">{{ formatPriceWithSymbol(service.unitPrice) }}</span>
  76. </div>
  77. </div>
  78. <div slot="footer">
  79. <el-button @click="servicesVisible = false">关闭</el-button>
  80. </div>
  81. </el-dialog>
  82. </div>
  83. </template>
  84. <script>
  85. import { getAllPackageTemplates, createPackageTemplate, updatePackageTemplate } from '@/api/adminService'
  86. export default {
  87. name: 'PackageTemplates',
  88. data() {
  89. return {
  90. templates: [],
  91. keyword: '',
  92. dialogVisible: false,
  93. servicesVisible: false,
  94. currentServices: [],
  95. isEdit: false,
  96. form: {
  97. id: null,
  98. name: '',
  99. validityMonths: 12,
  100. referencePrice: 0,
  101. minPrice: 0,
  102. description: '',
  103. status: 'ACTIVE'
  104. }
  105. }
  106. },
  107. computed: {
  108. filteredData() {
  109. if (!this.keyword) return this.templates
  110. const kw = this.keyword.toLowerCase()
  111. return this.templates.filter(item =>
  112. [item.name, item.description].some(v => (v||'').toLowerCase().includes(kw))
  113. )
  114. }
  115. },
  116. created() {
  117. this.loadData()
  118. },
  119. methods: {
  120. async loadData() {
  121. try {
  122. const res = await getAllPackageTemplates()
  123. if (res.data) {
  124. this.templates = res.data
  125. }
  126. } catch (e) {
  127. this.templates = [
  128. { id: 1, name: '年度旗舰版', validityMonths: 12, referencePrice: 1980, minPrice: 1280, description: '全年度服务', status: 'ACTIVE', services: [
  129. { serviceName: '1对1 DAN测评', quantity: 2, unit: '次/年', unitPrice: 800 },
  130. { serviceName: '成长报告', quantity: 12, unit: '份/年', unitPrice: 600 },
  131. { serviceName: '专属咨询', quantity: 999, unit: '次/年', unitPrice: 580 }
  132. ]},
  133. { id: 2, name: '季度体验版', validityMonths: 3, referencePrice: 580, minPrice: 380, description: '季度服务', status: 'ACTIVE', services: [
  134. { serviceName: '1对1 DAN测评', quantity: 1, unit: '次', unitPrice: 800 },
  135. { serviceName: '成长报告', quantity: 3, unit: '份', unitPrice: 50 }
  136. ]},
  137. { id: 3, name: '月度试用版', validityMonths: 1, referencePrice: 99, minPrice: 99, description: '月度试用', status: 'ACTIVE', services: [
  138. { serviceName: '1对1 DAN测评', quantity: 1, unit: '次', unitPrice: 800 }
  139. ]}
  140. ]
  141. }
  142. },
  143. handleCreate() {
  144. this.isEdit = false
  145. this.form = {
  146. id: null,
  147. name: '',
  148. validityMonths: 12,
  149. referencePrice: 0,
  150. minPrice: 0,
  151. description: '',
  152. status: 'ACTIVE'
  153. }
  154. this.dialogVisible = true
  155. },
  156. handleEdit(row) {
  157. this.isEdit = true
  158. this.form = { ...row }
  159. this.dialogVisible = true
  160. },
  161. handleViewServices(row) {
  162. this.currentServices = row.services || []
  163. this.servicesVisible = true
  164. },
  165. async toggleStatus(row) {
  166. row.status = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
  167. try {
  168. await updatePackageTemplate(row.id, row)
  169. this.$message.success('状态已更新')
  170. } catch (e) {
  171. row.status = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
  172. this.$message.error('更新失败')
  173. }
  174. },
  175. async handleSubmit() {
  176. if (!this.form.name || !this.form.validityMonths) {
  177. this.$message.warning('请填写完整信息')
  178. return
  179. }
  180. if (this.form.minPrice > this.form.referencePrice) {
  181. this.$message.warning('最低售价不能大于参考价格')
  182. return
  183. }
  184. try {
  185. if (this.isEdit) {
  186. await updatePackageTemplate(this.form.id, this.form)
  187. } else {
  188. await createPackageTemplate(this.form)
  189. }
  190. this.$message.success('保存成功')
  191. this.dialogVisible = false
  192. this.loadData()
  193. } catch (e) {
  194. this.$message.error('保存失败')
  195. }
  196. },
  197. handleActionCmd(row, cmd) {
  198. switch (cmd) {
  199. case 'viewServices': this.handleViewServices(row); break
  200. case 'toggleStatus': this.toggleStatus(row); break
  201. }
  202. },
  203. handleSearch() {}
  204. }
  205. }
  206. </script>
  207. <style scoped>
  208. .package-templates {
  209. padding: 20px;
  210. }
  211. .form-tip {
  212. font-size: 12px;
  213. color: #909399;
  214. margin-top: 4px;
  215. }
  216. .services-list {
  217. max-height: 400px;
  218. overflow-y: auto;
  219. }
  220. .service-item {
  221. display: flex;
  222. justify-content: space-between;
  223. align-items: center;
  224. padding: 15px;
  225. border-bottom: 1px solid #eee;
  226. }
  227. .service-item:last-child {
  228. border-bottom: none;
  229. }
  230. .service-info {
  231. display: flex;
  232. flex-direction: column;
  233. }
  234. .service-name {
  235. font-size: 14px;
  236. color: #333;
  237. }
  238. .service-qty {
  239. font-size: 12px;
  240. color: #999;
  241. margin-top: 4px;
  242. }
  243. .service-price {
  244. font-size: 16px;
  245. font-weight: bold;
  246. color: #409EFF;
  247. }
  248. .empty-tip {
  249. text-align: center;
  250. color: #909399;
  251. padding: 40px 0;
  252. }
  253. </style>