Commit 054f8469e8efa8b2eed1f9e744d8a3cf322dbef5
1 parent
fdbd231e
init
Showing
6 changed files
with
1364 additions
and
241 deletions
src/api/index.ts
| ... | ... | @@ -8,9 +8,23 @@ export const cityApi = { |
| 8 | 8 | edit: (data: any) => request.put('/api/platform/city/edit', data), |
| 9 | 9 | setStatus: (cityId: number, status: number) => |
| 10 | 10 | request.post('/api/platform/city/setStatus', null, { params: { cityId, status } }), |
| 11 | - getConfig: (cityId: number) => request.get(`/api/platform/city/config/${cityId}`), | |
| 12 | - updateConfig: (cityId: number, config: any) => | |
| 13 | - request.post(`/api/platform/city/config/${cityId}`, config), | |
| 11 | + listFeePlans: (cityId: number) => request.get(`/api/platform/city/${cityId}/fee-plans`), | |
| 12 | + getFeePlan: (cityId: number, planId: number) => | |
| 13 | + request.get(`/api/platform/city/${cityId}/fee-plans/${planId}`), | |
| 14 | + createFeePlan: (cityId: number, data: any) => | |
| 15 | + request.post(`/api/platform/city/${cityId}/fee-plans`, data), | |
| 16 | + initDefaultFeePlan: (cityId: number) => | |
| 17 | + request.post(`/api/platform/city/${cityId}/fee-plans/init-default`), | |
| 18 | + updateFeePlan: (cityId: number, planId: number, data: any) => | |
| 19 | + request.put(`/api/platform/city/${cityId}/fee-plans/${planId}`, data), | |
| 20 | + copyFeePlan: (cityId: number, planId: number) => | |
| 21 | + request.post(`/api/platform/city/${cityId}/fee-plans/${planId}/copy`), | |
| 22 | + setDefaultFeePlan: (cityId: number, planId: number) => | |
| 23 | + request.post(`/api/platform/city/${cityId}/fee-plans/${planId}/default`), | |
| 24 | + deleteFeePlan: (cityId: number, planId: number) => | |
| 25 | + request.delete(`/api/platform/city/${cityId}/fee-plans/${planId}`), | |
| 26 | + previewFeePlan: (cityId: number, data: any) => | |
| 27 | + request.post(`/api/platform/city/${cityId}/fee-plans/preview`, data), | |
| 14 | 28 | } |
| 15 | 29 | |
| 16 | 30 | // 分站管理 | ... | ... |
src/layouts/MainLayout.vue
| ... | ... | @@ -159,8 +159,8 @@ function handleLogout() { |
| 159 | 159 | min-height: 100vh; |
| 160 | 160 | display: grid; |
| 161 | 161 | grid-template-columns: 280px minmax(0, 1fr); |
| 162 | - gap: 22px; | |
| 163 | - padding: 22px; | |
| 162 | + gap: 16px; | |
| 163 | + padding: 16px; | |
| 164 | 164 | } |
| 165 | 165 | |
| 166 | 166 | .soft-sider, |
| ... | ... | @@ -173,10 +173,10 @@ function handleLogout() { |
| 173 | 173 | |
| 174 | 174 | .soft-sider { |
| 175 | 175 | position: sticky; |
| 176 | - top: 22px; | |
| 177 | - height: calc(100vh - 44px); | |
| 178 | - border-radius: 34px; | |
| 179 | - padding: 18px 14px 18px; | |
| 176 | + top: 16px; | |
| 177 | + height: calc(100vh - 32px); | |
| 178 | + border-radius: 26px; | |
| 179 | + padding: 14px 12px 14px; | |
| 180 | 180 | display: flex; |
| 181 | 181 | flex-direction: column; |
| 182 | 182 | overflow: hidden; |
| ... | ... | @@ -205,9 +205,9 @@ function handleLogout() { |
| 205 | 205 | |
| 206 | 206 | .sider-toggle { |
| 207 | 207 | align-self: flex-end; |
| 208 | - width: 44px; | |
| 209 | - height: 44px; | |
| 210 | - border-radius: 16px; | |
| 208 | + width: 38px; | |
| 209 | + height: 38px; | |
| 210 | + border-radius: 12px; | |
| 211 | 211 | border: none; |
| 212 | 212 | background: rgba(246, 242, 255, 0.9); |
| 213 | 213 | color: #7f6de5; |
| ... | ... | @@ -218,14 +218,14 @@ function handleLogout() { |
| 218 | 218 | .brand-block { |
| 219 | 219 | display: flex; |
| 220 | 220 | align-items: center; |
| 221 | - gap: 14px; | |
| 222 | - padding: 10px 10px 20px; | |
| 221 | + gap: 12px; | |
| 222 | + padding: 6px 8px 14px; | |
| 223 | 223 | } |
| 224 | 224 | |
| 225 | 225 | .brand-mark { |
| 226 | - width: 52px; | |
| 227 | - height: 52px; | |
| 228 | - border-radius: 18px; | |
| 226 | + width: 44px; | |
| 227 | + height: 44px; | |
| 228 | + border-radius: 14px; | |
| 229 | 229 | background: linear-gradient(145deg, #8c7cf0, #e6b5dc); |
| 230 | 230 | color: white; |
| 231 | 231 | font-family: 'Outfit', sans-serif; |
| ... | ... | @@ -240,6 +240,17 @@ function handleLogout() { |
| 240 | 240 | flex-direction: column; |
| 241 | 241 | } |
| 242 | 242 | |
| 243 | +.brand-copy strong { | |
| 244 | + font-size: 15px; | |
| 245 | + line-height: 1.35; | |
| 246 | +} | |
| 247 | + | |
| 248 | +.brand-copy span { | |
| 249 | + margin-top: 2px; | |
| 250 | + font-size: 11px; | |
| 251 | + line-height: 1.4; | |
| 252 | +} | |
| 253 | + | |
| 243 | 254 | .brand-copy strong, |
| 244 | 255 | .profile-copy strong, |
| 245 | 256 | .hero-copy h2, |
| ... | ... | @@ -264,11 +275,11 @@ h1 { |
| 264 | 275 | } |
| 265 | 276 | |
| 266 | 277 | .sider-foot { |
| 267 | - margin-top: 14px; | |
| 278 | + margin-top: 10px; | |
| 268 | 279 | flex-shrink: 0; |
| 269 | - border-radius: 24px; | |
| 280 | + border-radius: 18px; | |
| 270 | 281 | background: linear-gradient(180deg, rgba(245, 241, 255, 0.85), rgba(255, 247, 250, 0.92)); |
| 271 | - padding: 16px; | |
| 282 | + padding: 12px; | |
| 272 | 283 | } |
| 273 | 284 | |
| 274 | 285 | .soft-chip { |
| ... | ... | @@ -290,8 +301,8 @@ h1 { |
| 290 | 301 | } |
| 291 | 302 | |
| 292 | 303 | .soft-topbar { |
| 293 | - border-radius: 30px; | |
| 294 | - padding: 18px 24px; | |
| 304 | + border-radius: 22px; | |
| 305 | + padding: 12px 16px; | |
| 295 | 306 | display: flex; |
| 296 | 307 | align-items: center; |
| 297 | 308 | justify-content: space-between; |
| ... | ... | @@ -300,21 +311,22 @@ h1 { |
| 300 | 311 | |
| 301 | 312 | .soft-topbar h1 { |
| 302 | 313 | margin: 4px 0 0; |
| 303 | - font-size: 30px; | |
| 314 | + font-size: 22px; | |
| 315 | + line-height: 1.3; | |
| 304 | 316 | } |
| 305 | 317 | |
| 306 | 318 | .eyebrow { |
| 307 | 319 | margin: 0; |
| 308 | 320 | text-transform: uppercase; |
| 309 | 321 | letter-spacing: 0.12em; |
| 310 | - font-size: 11px; | |
| 322 | + font-size: 10px; | |
| 311 | 323 | font-weight: 700; |
| 312 | 324 | } |
| 313 | 325 | |
| 314 | 326 | .topbar-actions { |
| 315 | 327 | display: flex; |
| 316 | 328 | align-items: center; |
| 317 | - gap: 14px; | |
| 329 | + gap: 10px; | |
| 318 | 330 | } |
| 319 | 331 | |
| 320 | 332 | .date-pill, |
| ... | ... | @@ -325,7 +337,8 @@ h1 { |
| 325 | 337 | border-radius: 999px; |
| 326 | 338 | background: rgba(255, 255, 255, 0.82); |
| 327 | 339 | border: 1px solid rgba(194, 184, 237, 0.38); |
| 328 | - padding: 10px 14px; | |
| 340 | + padding: 7px 10px; | |
| 341 | + font-size: 12px; | |
| 329 | 342 | } |
| 330 | 343 | |
| 331 | 344 | .profile-button { |
| ... | ... | @@ -334,14 +347,15 @@ h1 { |
| 334 | 347 | } |
| 335 | 348 | |
| 336 | 349 | .profile-avatar { |
| 337 | - width: 38px; | |
| 338 | - height: 38px; | |
| 339 | - border-radius: 14px; | |
| 350 | + width: 34px; | |
| 351 | + height: 34px; | |
| 352 | + border-radius: 12px; | |
| 340 | 353 | display: grid; |
| 341 | 354 | place-items: center; |
| 342 | 355 | background: linear-gradient(135deg, #a48ef4, #f1bfd8); |
| 343 | 356 | color: white; |
| 344 | 357 | font-weight: 700; |
| 358 | + font-size: 13px; | |
| 345 | 359 | } |
| 346 | 360 | |
| 347 | 361 | .profile-copy { |
| ... | ... | @@ -350,6 +364,26 @@ h1 { |
| 350 | 364 | text-align: left; |
| 351 | 365 | } |
| 352 | 366 | |
| 367 | +.profile-copy strong { | |
| 368 | + font-size: 13px; | |
| 369 | + line-height: 1.35; | |
| 370 | +} | |
| 371 | + | |
| 372 | +.profile-copy small { | |
| 373 | + font-size: 11px; | |
| 374 | + line-height: 1.35; | |
| 375 | +} | |
| 376 | + | |
| 377 | +:deep(.ant-menu-item), | |
| 378 | +:deep(.ant-menu-submenu-title) { | |
| 379 | + min-height: 38px !important; | |
| 380 | + line-height: 38px !important; | |
| 381 | +} | |
| 382 | + | |
| 383 | +:deep(.ant-menu-title-content) { | |
| 384 | + font-size: 13px; | |
| 385 | +} | |
| 386 | + | |
| 353 | 387 | @media (max-width: 960px) { |
| 354 | 388 | .layout-shell { |
| 355 | 389 | grid-template-columns: 1fr; |
| ... | ... | @@ -378,7 +412,7 @@ h1 { |
| 378 | 412 | } |
| 379 | 413 | |
| 380 | 414 | .soft-topbar h1 { |
| 381 | - font-size: 26px; | |
| 415 | + font-size: 20px; | |
| 382 | 416 | } |
| 383 | 417 | |
| 384 | 418 | .hero-copy h2 { | ... | ... |
src/style.css
| ... | ... | @@ -30,6 +30,10 @@ |
| 30 | 30 | --radius-sm: 14px; |
| 31 | 31 | --font-display: 'Outfit', 'Avenir Next', 'Segoe UI', sans-serif; |
| 32 | 32 | --font-body: 'Plus Jakarta Sans', 'Segoe UI', sans-serif; |
| 33 | + --font-size-base: 14px; | |
| 34 | + --font-size-sm: 12px; | |
| 35 | + --font-size-md: 13px; | |
| 36 | + --font-size-lg: 15px; | |
| 33 | 37 | color: var(--text-main); |
| 34 | 38 | font-family: var(--font-body); |
| 35 | 39 | line-height: 1.5; |
| ... | ... | @@ -55,6 +59,7 @@ body { |
| 55 | 59 | margin: 0; |
| 56 | 60 | background: var(--app-bg); |
| 57 | 61 | color: var(--text-main); |
| 62 | + font-size: var(--font-size-base); | |
| 58 | 63 | } |
| 59 | 64 | |
| 60 | 65 | a { |
| ... | ... | @@ -69,6 +74,9 @@ a { |
| 69 | 74 | border-radius: 999px; |
| 70 | 75 | font-weight: 600; |
| 71 | 76 | box-shadow: none; |
| 77 | + font-size: var(--font-size-md); | |
| 78 | + height: 32px; | |
| 79 | + padding-inline: 14px; | |
| 72 | 80 | } |
| 73 | 81 | |
| 74 | 82 | .ant-btn-primary { |
| ... | ... | @@ -97,18 +105,19 @@ a { |
| 97 | 105 | |
| 98 | 106 | .ant-card .ant-card-head { |
| 99 | 107 | border-bottom: 1px solid rgba(188, 180, 230, 0.18); |
| 100 | - min-height: 72px; | |
| 108 | + min-height: 56px; | |
| 109 | + padding-inline: 20px; | |
| 101 | 110 | } |
| 102 | 111 | |
| 103 | 112 | .ant-card .ant-card-head-title { |
| 104 | 113 | font-family: var(--font-display); |
| 105 | - font-size: 1.1rem; | |
| 114 | + font-size: var(--font-size-lg); | |
| 106 | 115 | color: var(--text-dark); |
| 107 | 116 | font-weight: 700; |
| 108 | 117 | } |
| 109 | 118 | |
| 110 | 119 | .ant-card .ant-card-body { |
| 111 | - padding: 24px; | |
| 120 | + padding: 18px 20px; | |
| 112 | 121 | } |
| 113 | 122 | |
| 114 | 123 | .ant-table-wrapper .ant-table { |
| ... | ... | @@ -126,11 +135,17 @@ a { |
| 126 | 135 | color: var(--text-dark); |
| 127 | 136 | border-bottom: none; |
| 128 | 137 | font-weight: 700; |
| 138 | + font-size: var(--font-size-md); | |
| 139 | + padding-top: 12px; | |
| 140 | + padding-bottom: 12px; | |
| 129 | 141 | } |
| 130 | 142 | |
| 131 | 143 | .ant-table-wrapper .ant-table-tbody > tr > td { |
| 132 | 144 | border-bottom: 1px solid rgba(226, 220, 247, 0.72); |
| 133 | 145 | background: rgba(255, 255, 255, 0.48); |
| 146 | + font-size: var(--font-size-md); | |
| 147 | + padding-top: 11px; | |
| 148 | + padding-bottom: 11px; | |
| 134 | 149 | } |
| 135 | 150 | |
| 136 | 151 | .ant-table-wrapper .ant-table-tbody > tr:hover > td { |
| ... | ... | @@ -148,6 +163,30 @@ a { |
| 148 | 163 | border-color: rgba(189, 180, 234, 0.4) !important; |
| 149 | 164 | background: rgba(255, 255, 255, 0.78) !important; |
| 150 | 165 | box-shadow: none !important; |
| 166 | + font-size: var(--font-size-md); | |
| 167 | +} | |
| 168 | + | |
| 169 | +.ant-input, | |
| 170 | +.ant-input-affix-wrapper, | |
| 171 | +.ant-input-password, | |
| 172 | +.ant-picker { | |
| 173 | + min-height: 34px; | |
| 174 | + padding-top: 5px; | |
| 175 | + padding-bottom: 5px; | |
| 176 | +} | |
| 177 | + | |
| 178 | +.ant-input-number { | |
| 179 | + min-height: 34px; | |
| 180 | +} | |
| 181 | + | |
| 182 | +.ant-input-number-input { | |
| 183 | + height: 32px; | |
| 184 | +} | |
| 185 | + | |
| 186 | +.ant-select-selector { | |
| 187 | + min-height: 34px !important; | |
| 188 | + padding-top: 1px !important; | |
| 189 | + padding-bottom: 1px !important; | |
| 151 | 190 | } |
| 152 | 191 | |
| 153 | 192 | .ant-input:focus, |
| ... | ... | @@ -161,7 +200,7 @@ a { |
| 161 | 200 | |
| 162 | 201 | .ant-modal .ant-modal-content, |
| 163 | 202 | .ant-dropdown .ant-dropdown-menu { |
| 164 | - border-radius: 28px; | |
| 203 | + border-radius: 22px; | |
| 165 | 204 | border: 1px solid rgba(228, 223, 247, 0.7); |
| 166 | 205 | background: rgba(255, 255, 255, 0.92); |
| 167 | 206 | backdrop-filter: blur(24px); |
| ... | ... | @@ -171,12 +210,22 @@ a { |
| 171 | 210 | .ant-modal .ant-modal-header { |
| 172 | 211 | background: transparent; |
| 173 | 212 | border-bottom: 1px solid rgba(189, 180, 234, 0.18); |
| 213 | + padding: 18px 20px 12px; | |
| 174 | 214 | } |
| 175 | 215 | |
| 176 | 216 | .ant-modal .ant-modal-title { |
| 177 | 217 | font-family: var(--font-display); |
| 178 | 218 | color: var(--text-dark); |
| 179 | 219 | font-weight: 700; |
| 220 | + font-size: var(--font-size-lg); | |
| 221 | +} | |
| 222 | + | |
| 223 | +.ant-modal .ant-modal-body { | |
| 224 | + padding: 18px 20px 20px; | |
| 225 | +} | |
| 226 | + | |
| 227 | +.ant-modal .ant-modal-footer { | |
| 228 | + padding: 12px 20px 18px; | |
| 180 | 229 | } |
| 181 | 230 | |
| 182 | 231 | .ant-tag { |
| ... | ... | @@ -184,18 +233,32 @@ a { |
| 184 | 233 | border: none; |
| 185 | 234 | padding-inline: 10px; |
| 186 | 235 | font-weight: 600; |
| 236 | + font-size: var(--font-size-sm); | |
| 187 | 237 | } |
| 188 | 238 | |
| 189 | 239 | .ant-menu { |
| 190 | 240 | background: transparent !important; |
| 241 | + font-size: var(--font-size-md); | |
| 191 | 242 | } |
| 192 | 243 | |
| 193 | 244 | .ant-menu-item, |
| 194 | 245 | .ant-menu-submenu-title { |
| 195 | - border-radius: 18px !important; | |
| 246 | + border-radius: 14px !important; | |
| 196 | 247 | margin-inline: 8px !important; |
| 197 | - margin-block: 6px !important; | |
| 248 | + margin-block: 4px !important; | |
| 198 | 249 | width: calc(100% - 16px) !important; |
| 250 | + font-size: var(--font-size-md) !important; | |
| 251 | +} | |
| 252 | + | |
| 253 | +.ant-form-item { | |
| 254 | + margin-bottom: 16px; | |
| 255 | +} | |
| 256 | + | |
| 257 | +.ant-form-item .ant-form-item-label > label, | |
| 258 | +.ant-empty-description, | |
| 259 | +.ant-dropdown-menu-item, | |
| 260 | +.ant-pagination { | |
| 261 | + font-size: var(--font-size-md); | |
| 199 | 262 | } |
| 200 | 263 | |
| 201 | 264 | .ant-menu-light .ant-menu-item-selected, |
| ... | ... | @@ -223,7 +286,7 @@ a { |
| 223 | 286 | } |
| 224 | 287 | |
| 225 | 288 | .soft-page-shell { |
| 226 | - padding: 24px 24px 28px; | |
| 289 | + padding: 18px 18px 22px; | |
| 227 | 290 | } |
| 228 | 291 | |
| 229 | 292 | .soft-section-card { | ... | ... |
src/views/city/CityList.vue
| ... | ... | @@ -4,8 +4,7 @@ |
| 4 | 4 | <template #extra> |
| 5 | 5 | <a-button type="primary" @click="openAdd">新增</a-button> |
| 6 | 6 | </template> |
| 7 | - <a-table :dataSource="list" :columns="columns" :loading="loading" | |
| 8 | - rowKey="id" :pagination="false"> | |
| 7 | + <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> | |
| 9 | 8 | <template #bodyCell="{ column, record }"> |
| 10 | 9 | <template v-if="column.key === 'status'"> |
| 11 | 10 | <a-tag :color="record.status === 1 ? 'green' : 'default'"> |
| ... | ... | @@ -17,9 +16,7 @@ |
| 17 | 16 | <a @click="openEdit(record)">编辑</a> |
| 18 | 17 | <a @click="openConfig(record)">配送费配置</a> |
| 19 | 18 | <a @click="openLevelConfig(record)">骑手等级</a> |
| 20 | - <a-popconfirm | |
| 21 | - :title="record.status === 1 ? '确认关闭?' : '确认开通?'" | |
| 22 | - @confirm="toggleStatus(record)"> | |
| 19 | + <a-popconfirm :title="record.status === 1 ? '确认关闭?' : '确认开通?'" @confirm="toggleStatus(record)"> | |
| 23 | 20 | <a>{{ record.status === 1 ? '关闭' : '开通' }}</a> |
| 24 | 21 | </a-popconfirm> |
| 25 | 22 | </a-space> |
| ... | ... | @@ -28,9 +25,7 @@ |
| 28 | 25 | </a-table> |
| 29 | 26 | </a-card> |
| 30 | 27 | |
| 31 | - <!-- 新增/编辑弹窗 --> | |
| 32 | - <a-modal v-model:open="modalVisible" :title="editingId ? '编辑' : '新增租户'" | |
| 33 | - @ok="handleSave" :confirmLoading="saving"> | |
| 28 | + <a-modal v-model:open="modalVisible" :title="editingId ? '编辑' : '新增租户'" @ok="handleSave" :confirmLoading="saving"> | |
| 34 | 29 | <a-form :model="form" layout="vertical"> |
| 35 | 30 | <a-form-item label="名称"> |
| 36 | 31 | <a-input v-model:value="form.name" placeholder="如:华东一区 / 某租户名" /> |
| ... | ... | @@ -47,161 +42,331 @@ |
| 47 | 42 | </a-form> |
| 48 | 43 | </a-modal> |
| 49 | 44 | |
| 50 | - <!-- 配送费配置弹窗 --> | |
| 51 | - <a-modal v-model:open="configVisible" title="配送费配置" width="760px" | |
| 52 | - @ok="handleConfigSave" :confirmLoading="saving"> | |
| 53 | - <a-form :model="config" layout="vertical" v-if="config"> | |
| 54 | - <a-form-item label="计费模式"> | |
| 55 | - <a-radio-group v-model:value="config.type6.feeMode"> | |
| 56 | - <a-radio :value="1">固定费用</a-radio> | |
| 57 | - <a-radio :value="2">按距离/重量计费</a-radio> | |
| 58 | - </a-radio-group> | |
| 59 | - </a-form-item> | |
| 45 | + <a-modal v-model:open="configVisible" :title="`配送费配置 - ${currentCityName}`" width="1320px" :footer="null"> | |
| 46 | + <div class="plan-layout"> | |
| 47 | + <div class="plan-sidebar"> | |
| 48 | + <div class="plan-sidebar-header"> | |
| 49 | + <div> | |
| 50 | + <div class="plan-sidebar-title">计价方案</div> | |
| 51 | + <div class="plan-sidebar-subtitle">同一租户可维护多套外卖配送规则</div> | |
| 52 | + </div> | |
| 53 | + <a-button type="primary" size="small" @click="createPlan">新增</a-button> | |
| 54 | + </div> | |
| 55 | + <a-spin :spinning="planLoading"> | |
| 56 | + <div class="plan-list"> | |
| 57 | + <button | |
| 58 | + v-for="item in planList" | |
| 59 | + :key="item.id" | |
| 60 | + type="button" | |
| 61 | + class="plan-item" | |
| 62 | + :class="{ active: item.id === selectedPlanId }" | |
| 63 | + @click="selectPlan(item.id)" | |
| 64 | + > | |
| 65 | + <div class="plan-item-top"> | |
| 66 | + <span class="plan-item-name">{{ item.name }}</span> | |
| 67 | + <a-tag v-if="item.isDefault === 1" color="green">默认</a-tag> | |
| 68 | + </div> | |
| 69 | + <div class="plan-item-bottom"> | |
| 70 | + <span>{{ item.status === 1 ? '启用中' : '已停用' }}</span> | |
| 71 | + <span>排序 {{ item.listOrder ?? 0 }}</span> | |
| 72 | + </div> | |
| 73 | + </button> | |
| 74 | + <a-empty v-if="!planList.length" description="暂无计价方案" /> | |
| 75 | + </div> | |
| 76 | + </a-spin> | |
| 77 | + </div> | |
| 60 | 78 | |
| 61 | - <template v-if="config.type6.feeMode === 1"> | |
| 62 | - <a-form-item label="固定费用(元)"> | |
| 63 | - <a-input-number v-model:value="config.type6.fixMoney" :min="0" style="width:100%" /> | |
| 64 | - </a-form-item> | |
| 65 | - </template> | |
| 79 | + <div class="plan-content"> | |
| 80 | + <template v-if="currentPlan && config"> | |
| 81 | + <div class="plan-toolbar"> | |
| 82 | + <a-space wrap> | |
| 83 | + <a-button @click="copyPlan" :disabled="!selectedPlanId">复制当前方案</a-button> | |
| 84 | + <a-button @click="setDefaultPlan" :disabled="currentPlan.isDefault === 1 || currentPlan.status !== 1">设为默认</a-button> | |
| 85 | + <a-popconfirm title="确认删除当前方案?" @confirm="deletePlan"> | |
| 86 | + <a-button danger :disabled="currentPlan.isDefault === 1">删除方案</a-button> | |
| 87 | + </a-popconfirm> | |
| 88 | + </a-space> | |
| 89 | + <a-space wrap> | |
| 90 | + <a-button @click="previewPlan" :loading="previewing">试算配送费</a-button> | |
| 91 | + <a-button type="primary" @click="saveCurrentPlan" :loading="planSaving">保存当前方案</a-button> | |
| 92 | + </a-space> | |
| 93 | + </div> | |
| 66 | 94 | |
| 67 | - <template v-else> | |
| 68 | - <a-divider orientation="left">距离计费</a-divider> | |
| 69 | - <a-form-item label="启用距离计费"> | |
| 70 | - <a-radio-group v-model:value="config.type6.distanceSwitch"> | |
| 71 | - <a-radio :value="1">启用</a-radio> | |
| 72 | - <a-radio :value="0">关闭</a-radio> | |
| 73 | - </a-radio-group> | |
| 74 | - </a-form-item> | |
| 75 | - <template v-if="config.type6.distanceSwitch === 1"> | |
| 76 | - <a-row :gutter="16"> | |
| 77 | - <a-col :span="12"> | |
| 78 | - <a-form-item label="基础距离(km)"> | |
| 79 | - <a-input-number v-model:value="config.type6.distanceBasic" :min="0" :step="0.1" style="width:100%" /> | |
| 80 | - </a-form-item> | |
| 81 | - </a-col> | |
| 82 | - <a-col :span="12"> | |
| 83 | - <a-form-item label="基础距离内费用(元)"> | |
| 84 | - <a-input-number v-model:value="config.type6.distanceBasicMoney" :min="0" :step="0.1" style="width:100%" /> | |
| 85 | - </a-form-item> | |
| 86 | - </a-col> | |
| 87 | - </a-row> | |
| 88 | - <a-row :gutter="16"> | |
| 89 | - <a-col :span="12"> | |
| 90 | - <a-form-item label="超出每km费用(元)"> | |
| 91 | - <a-input-number v-model:value="config.type6.distanceMoreMoney" :min="0" :step="0.1" style="width:100%" /> | |
| 92 | - </a-form-item> | |
| 93 | - </a-col> | |
| 94 | - <a-col :span="12"> | |
| 95 | - <a-form-item label="距离取整方式"> | |
| 96 | - <a-select v-model:value="config.type6.distanceType"> | |
| 97 | - <a-select-option :value="1">四舍五入</a-select-option> | |
| 98 | - <a-select-option :value="2">向上取整</a-select-option> | |
| 99 | - <a-select-option :value="3">向下取整</a-select-option> | |
| 100 | - </a-select> | |
| 101 | - </a-form-item> | |
| 102 | - </a-col> | |
| 103 | - </a-row> | |
| 104 | - </template> | |
| 105 | - | |
| 106 | - <a-divider orientation="left">重量计费</a-divider> | |
| 107 | - <a-form-item label="启用重量计费"> | |
| 108 | - <a-radio-group v-model:value="config.type6.weightSwitch"> | |
| 109 | - <a-radio :value="1">启用</a-radio> | |
| 110 | - <a-radio :value="0">关闭</a-radio> | |
| 111 | - </a-radio-group> | |
| 112 | - </a-form-item> | |
| 113 | - <template v-if="config.type6.weightSwitch === 1"> | |
| 114 | 95 | <a-row :gutter="16"> |
| 115 | - <a-col :span="12"> | |
| 116 | - <a-form-item label="基础重量(kg)"> | |
| 117 | - <a-input-number v-model:value="config.type6.weightBasic" :min="0" :step="0.1" style="width:100%" /> | |
| 96 | + <a-col :span="8"> | |
| 97 | + <a-form-item label="方案名称"> | |
| 98 | + <a-input v-model:value="currentPlan.name" placeholder="如:标准午高峰方案" /> | |
| 118 | 99 | </a-form-item> |
| 119 | 100 | </a-col> |
| 120 | - <a-col :span="12"> | |
| 121 | - <a-form-item label="基础重量内费用(元)"> | |
| 122 | - <a-input-number v-model:value="config.type6.weightBasicMoney" :min="0" :step="0.1" style="width:100%" /> | |
| 123 | - </a-form-item> | |
| 124 | - </a-col> | |
| 125 | - </a-row> | |
| 126 | - <a-row :gutter="16"> | |
| 127 | - <a-col :span="12"> | |
| 128 | - <a-form-item label="超出每kg费用(元)"> | |
| 129 | - <a-input-number v-model:value="config.type6.weightMoreMoney" :min="0" :step="0.1" style="width:100%" /> | |
| 130 | - </a-form-item> | |
| 131 | - </a-col> | |
| 132 | - <a-col :span="12"> | |
| 133 | - <a-form-item label="重量取整方式"> | |
| 134 | - <a-select v-model:value="config.type6.weightType"> | |
| 135 | - <a-select-option :value="1">四舍五入</a-select-option> | |
| 136 | - <a-select-option :value="2">向上取整</a-select-option> | |
| 137 | - <a-select-option :value="3">向下取整</a-select-option> | |
| 138 | - </a-select> | |
| 139 | - </a-form-item> | |
| 140 | - </a-col> | |
| 141 | - </a-row> | |
| 142 | - </template> | |
| 143 | - | |
| 144 | - <a-divider orientation="left">时段附加费</a-divider> | |
| 145 | - <div style="margin-bottom:12px"> | |
| 146 | - <a-button type="dashed" block @click="addTimePeriod">新增时段</a-button> | |
| 147 | - </div> | |
| 148 | - <div v-if="timePeriods.length"> | |
| 149 | - <a-row | |
| 150 | - v-for="(period, index) in timePeriods" | |
| 151 | - :key="index" | |
| 152 | - :gutter="12" | |
| 153 | - style="margin-bottom:12px;align-items:flex-start" | |
| 154 | - > | |
| 155 | - <a-col :span="5"> | |
| 156 | - <a-form-item :label="index === 0 ? '开始时间' : ''"> | |
| 157 | - <a-input v-model:value="period.startText" placeholder="08:00" /> | |
| 158 | - </a-form-item> | |
| 159 | - </a-col> | |
| 160 | - <a-col :span="5"> | |
| 161 | - <a-form-item :label="index === 0 ? '结束时间' : ''"> | |
| 162 | - <a-input v-model:value="period.endText" placeholder="22:00" /> | |
| 163 | - </a-form-item> | |
| 164 | - </a-col> | |
| 165 | - <a-col :span="5"> | |
| 166 | - <a-form-item :label="index === 0 ? '附加费(元)' : ''"> | |
| 167 | - <a-input-number v-model:value="period.money" :min="0" :step="0.1" style="width:100%" /> | |
| 168 | - </a-form-item> | |
| 169 | - </a-col> | |
| 170 | - <a-col :span="5"> | |
| 171 | - <a-form-item :label="index === 0 ? '状态' : ''"> | |
| 172 | - <a-select v-model:value="period.isOpen"> | |
| 101 | + <a-col :span="8"> | |
| 102 | + <a-form-item label="状态"> | |
| 103 | + <a-select v-model:value="currentPlan.status"> | |
| 173 | 104 | <a-select-option :value="1">启用</a-select-option> |
| 174 | - <a-select-option :value="0">关闭</a-select-option> | |
| 105 | + <a-select-option :value="0">停用</a-select-option> | |
| 175 | 106 | </a-select> |
| 176 | 107 | </a-form-item> |
| 177 | 108 | </a-col> |
| 178 | - <a-col :span="4"> | |
| 179 | - <a-form-item :label="index === 0 ? '操作' : ''"> | |
| 180 | - <a-button danger block @click="removeTimePeriod(index)">删除</a-button> | |
| 109 | + <a-col :span="8"> | |
| 110 | + <a-form-item label="排序"> | |
| 111 | + <a-input-number v-model:value="currentPlan.listOrder" :min="0" style="width:100%" /> | |
| 181 | 112 | </a-form-item> |
| 182 | 113 | </a-col> |
| 183 | 114 | </a-row> |
| 184 | - </div> | |
| 185 | - <a-empty v-else description="暂无时段附加费配置" /> | |
| 186 | - </template> | |
| 187 | - | |
| 188 | - <a-divider orientation="left">预计送达与展示</a-divider> | |
| 189 | - <a-row :gutter="16"> | |
| 190 | - <a-col :span="12"> | |
| 191 | - <a-form-item label="预计送达基础时间(分钟)"> | |
| 192 | - <a-input-number v-model:value="config.distanceBasicTime" :min="0" style="width:100%" /> | |
| 193 | - </a-form-item> | |
| 194 | - </a-col> | |
| 195 | - <a-col :span="12"> | |
| 196 | - <a-form-item label="超出每km增加时间(分钟)"> | |
| 197 | - <a-input-number v-model:value="config.distanceMoreTime" :min="0" style="width:100%" /> | |
| 115 | + <a-form-item label="备注"> | |
| 116 | + <a-input v-model:value="currentPlan.remark" placeholder="可填写适用业务场景说明" /> | |
| 198 | 117 | </a-form-item> |
| 199 | - </a-col> | |
| 200 | - </a-row> | |
| 201 | - <a-form-item label="附近骑手显示范围(km)"> | |
| 202 | - <a-input-number v-model:value="config.riderDistance" :min="0" :step="0.1" style="width:100%" /> | |
| 203 | - </a-form-item> | |
| 204 | - </a-form> | |
| 118 | + | |
| 119 | + <a-card class="preview-card" :bordered="false"> | |
| 120 | + <template #title>草稿试算</template> | |
| 121 | + <a-row :gutter="12"> | |
| 122 | + <a-col :span="6"> | |
| 123 | + <a-form-item label="起点经度"> | |
| 124 | + <a-input v-model:value="previewForm.startLng" placeholder="121.4737" /> | |
| 125 | + </a-form-item> | |
| 126 | + </a-col> | |
| 127 | + <a-col :span="6"> | |
| 128 | + <a-form-item label="起点纬度"> | |
| 129 | + <a-input v-model:value="previewForm.startLat" placeholder="31.2304" /> | |
| 130 | + </a-form-item> | |
| 131 | + </a-col> | |
| 132 | + <a-col :span="6"> | |
| 133 | + <a-form-item label="终点经度"> | |
| 134 | + <a-input v-model:value="previewForm.endLng" placeholder="121.4879" /> | |
| 135 | + </a-form-item> | |
| 136 | + </a-col> | |
| 137 | + <a-col :span="6"> | |
| 138 | + <a-form-item label="终点纬度"> | |
| 139 | + <a-input v-model:value="previewForm.endLat" placeholder="31.2492" /> | |
| 140 | + </a-form-item> | |
| 141 | + </a-col> | |
| 142 | + </a-row> | |
| 143 | + <a-row :gutter="12"> | |
| 144 | + <a-col :span="8"> | |
| 145 | + <a-form-item label="重量(kg)"> | |
| 146 | + <a-input-number v-model:value="previewForm.weight" :min="0" :step="0.1" style="width:100%" /> | |
| 147 | + </a-form-item> | |
| 148 | + </a-col> | |
| 149 | + <a-col :span="8"> | |
| 150 | + <a-form-item label="件数"> | |
| 151 | + <a-input-number v-model:value="previewForm.pieces" :min="0" style="width:100%" /> | |
| 152 | + </a-form-item> | |
| 153 | + </a-col> | |
| 154 | + <a-col :span="8"> | |
| 155 | + <a-form-item label="服务时间戳(秒,可空)"> | |
| 156 | + <a-input v-model:value="previewForm.serviceTime" placeholder="留空则按当前时间" /> | |
| 157 | + </a-form-item> | |
| 158 | + </a-col> | |
| 159 | + </a-row> | |
| 160 | + <div v-if="previewResult" class="preview-result"> | |
| 161 | + <a-tag color="processing">总配送费 {{ previewResult.totalFee ?? 0 }} 元</a-tag> | |
| 162 | + <a-tag>基础 {{ previewResult.moneyBasic ?? 0 }}</a-tag> | |
| 163 | + <a-tag>里程 {{ previewResult.moneyDistance ?? 0 }}</a-tag> | |
| 164 | + <a-tag>重量 {{ previewResult.moneyWeight ?? 0 }}</a-tag> | |
| 165 | + <a-tag>件数 {{ previewResult.moneyPiece ?? 0 }}</a-tag> | |
| 166 | + <a-tag>时段 {{ previewResult.moneyTime ?? 0 }}</a-tag> | |
| 167 | + <a-tag>预计送达 {{ previewResult.estimatedMinutes ?? 0 }} 分钟</a-tag> | |
| 168 | + <a-tag>里程 {{ previewResult.distance ?? 0 }} km</a-tag> | |
| 169 | + <a-tag v-if="previewResult.minFeeApplied === 1" color="gold">已触发保底 {{ previewResult.minFee ?? 0 }}</a-tag> | |
| 170 | + <a-tag v-if="previewResult.moneyTime > 0" color="purple">当前服务时间命中时段加价</a-tag> | |
| 171 | + </div> | |
| 172 | + </a-card> | |
| 173 | + | |
| 174 | + <a-form :model="config" layout="vertical"> | |
| 175 | + <a-divider orientation="left">费用总览</a-divider> | |
| 176 | + <a-row :gutter="16"> | |
| 177 | + <a-col :span="12"> | |
| 178 | + <a-form-item label="保底费用(元)"> | |
| 179 | + <a-input-number v-model:value="config.type6.minFee" :min="0" :step="0.1" style="width:100%" /> | |
| 180 | + </a-form-item> | |
| 181 | + </a-col> | |
| 182 | + <a-col :span="12"> | |
| 183 | + <a-form-item label="基础费(元/单)"> | |
| 184 | + <a-input-number v-model:value="config.type6.baseFee" :min="0" :step="0.1" style="width:100%" /> | |
| 185 | + </a-form-item> | |
| 186 | + </a-col> | |
| 187 | + </a-row> | |
| 188 | + | |
| 189 | + <a-divider orientation="left">里程阶梯</a-divider> | |
| 190 | + <a-row :gutter="16"> | |
| 191 | + <a-col :span="12"> | |
| 192 | + <a-form-item label="起步里程(km内)"> | |
| 193 | + <a-input-number v-model:value="config.type6.distanceBasic" :min="0" :step="0.1" style="width:100%" /> | |
| 194 | + </a-form-item> | |
| 195 | + </a-col> | |
| 196 | + <a-col :span="12"> | |
| 197 | + <a-form-item label="起步费用(元)"> | |
| 198 | + <a-input-number v-model:value="config.type6.distanceBasicMoney" :min="0" :step="0.1" style="width:100%" /> | |
| 199 | + </a-form-item> | |
| 200 | + </a-col> | |
| 201 | + </a-row> | |
| 202 | + <div style="margin-bottom:12px"> | |
| 203 | + <a-button type="dashed" block @click="addDistanceStep">新增阶梯段</a-button> | |
| 204 | + </div> | |
| 205 | + <div v-if="distanceSteps.length"> | |
| 206 | + <a-row | |
| 207 | + v-for="(step, index) in distanceSteps" | |
| 208 | + :key="index" | |
| 209 | + :gutter="12" | |
| 210 | + style="margin-bottom:12px;align-items:flex-start" | |
| 211 | + > | |
| 212 | + <a-col :span="7"> | |
| 213 | + <a-form-item :label="index === 0 ? '结束里程(km)' : ''"> | |
| 214 | + <a-input-number v-model:value="step.endDistance" :min="0" :step="0.1" style="width:100%" /> | |
| 215 | + </a-form-item> | |
| 216 | + </a-col> | |
| 217 | + <a-col :span="7"> | |
| 218 | + <a-form-item :label="index === 0 ? '每档里程(km)' : ''"> | |
| 219 | + <a-input-number v-model:value="step.unitDistance" :min="0.1" :step="0.1" style="width:100%" /> | |
| 220 | + </a-form-item> | |
| 221 | + </a-col> | |
| 222 | + <a-col :span="7"> | |
| 223 | + <a-form-item :label="index === 0 ? '每档加价(元)' : ''"> | |
| 224 | + <a-input-number v-model:value="step.unitFee" :min="0" :step="0.1" style="width:100%" /> | |
| 225 | + </a-form-item> | |
| 226 | + </a-col> | |
| 227 | + <a-col :span="3"> | |
| 228 | + <a-form-item :label="index === 0 ? '操作' : ''"> | |
| 229 | + <a-button danger block @click="removeDistanceStep(index)">删除</a-button> | |
| 230 | + </a-form-item> | |
| 231 | + </a-col> | |
| 232 | + </a-row> | |
| 233 | + </div> | |
| 234 | + <a-empty v-else description="暂无里程阶梯配置" /> | |
| 235 | + | |
| 236 | + <a-divider orientation="left">重量计费</a-divider> | |
| 237 | + <a-row :gutter="16"> | |
| 238 | + <a-col :span="12"> | |
| 239 | + <a-form-item label="首重(kg)"> | |
| 240 | + <a-input-number v-model:value="config.type6.weightFirst" :min="0" :step="0.1" style="width:100%" /> | |
| 241 | + </a-form-item> | |
| 242 | + </a-col> | |
| 243 | + <a-col :span="12"> | |
| 244 | + <a-form-item label="首重费用(元)"> | |
| 245 | + <a-input-number v-model:value="config.type6.weightFirstFee" :min="0" :step="0.1" style="width:100%" /> | |
| 246 | + </a-form-item> | |
| 247 | + </a-col> | |
| 248 | + </a-row> | |
| 249 | + <a-row :gutter="16"> | |
| 250 | + <a-col :span="12"> | |
| 251 | + <a-form-item label="续重单价(元/kg)"> | |
| 252 | + <a-input-number v-model:value="config.type6.weightUnitFee" :min="0" :step="0.1" style="width:100%" /> | |
| 253 | + </a-form-item> | |
| 254 | + </a-col> | |
| 255 | + <a-col :span="12"> | |
| 256 | + <a-form-item label="封顶费用(元)"> | |
| 257 | + <a-input-number v-model:value="config.type6.weightCapFee" :min="0" :step="0.1" style="width:100%" /> | |
| 258 | + </a-form-item> | |
| 259 | + </a-col> | |
| 260 | + </a-row> | |
| 261 | + | |
| 262 | + <a-divider orientation="left">件数计费</a-divider> | |
| 263 | + <div style="margin-bottom:12px"> | |
| 264 | + <a-button type="dashed" block @click="addPieceRule">新增件数区间</a-button> | |
| 265 | + </div> | |
| 266 | + <div v-if="pieceRules.length"> | |
| 267 | + <a-row | |
| 268 | + v-for="(rule, index) in pieceRules" | |
| 269 | + :key="index" | |
| 270 | + :gutter="12" | |
| 271 | + style="margin-bottom:12px;align-items:flex-start" | |
| 272 | + > | |
| 273 | + <a-col :span="6"> | |
| 274 | + <a-form-item :label="index === 0 ? '起始件数' : ''"> | |
| 275 | + <a-input-number v-model:value="rule.startPiece" :min="0" style="width:100%" /> | |
| 276 | + </a-form-item> | |
| 277 | + </a-col> | |
| 278 | + <a-col :span="6"> | |
| 279 | + <a-form-item :label="index === 0 ? '结束件数' : ''"> | |
| 280 | + <a-input-number v-model:value="rule.endPiece" :min="0" style="width:100%" /> | |
| 281 | + </a-form-item> | |
| 282 | + </a-col> | |
| 283 | + <a-col :span="8"> | |
| 284 | + <a-form-item :label="index === 0 ? '费用(元)' : ''"> | |
| 285 | + <a-input-number v-model:value="rule.fee" :min="0" :step="0.1" style="width:100%" /> | |
| 286 | + </a-form-item> | |
| 287 | + </a-col> | |
| 288 | + <a-col :span="4"> | |
| 289 | + <a-form-item :label="index === 0 ? '操作' : ''"> | |
| 290 | + <a-button danger block @click="removePieceRule(index)">删除</a-button> | |
| 291 | + </a-form-item> | |
| 292 | + </a-col> | |
| 293 | + </a-row> | |
| 294 | + </div> | |
| 295 | + <a-empty v-else description="暂无件数区间配置" /> | |
| 296 | + | |
| 297 | + <a-divider orientation="left">时段附加费</a-divider> | |
| 298 | + <div style="margin-bottom:12px"> | |
| 299 | + <a-button type="dashed" block @click="addTimePeriod">新增时段</a-button> | |
| 300 | + </div> | |
| 301 | + <div v-if="timePeriods.length"> | |
| 302 | + <a-row | |
| 303 | + v-for="(period, index) in timePeriods" | |
| 304 | + :key="index" | |
| 305 | + :gutter="12" | |
| 306 | + style="margin-bottom:12px;align-items:flex-start" | |
| 307 | + > | |
| 308 | + <a-col :span="5"> | |
| 309 | + <a-form-item :label="index === 0 ? '开始时间' : ''"> | |
| 310 | + <a-input v-model:value="period.startText" placeholder="22:00" /> | |
| 311 | + </a-form-item> | |
| 312 | + </a-col> | |
| 313 | + <a-col :span="5"> | |
| 314 | + <a-form-item :label="index === 0 ? '结束时间' : ''"> | |
| 315 | + <a-input v-model:value="period.endText" placeholder="06:00" /> | |
| 316 | + </a-form-item> | |
| 317 | + </a-col> | |
| 318 | + <a-col :span="5"> | |
| 319 | + <a-form-item :label="index === 0 ? '附加费(元)' : ''"> | |
| 320 | + <a-input-number v-model:value="period.money" :min="0" :step="0.1" style="width:100%" /> | |
| 321 | + </a-form-item> | |
| 322 | + </a-col> | |
| 323 | + <a-col :span="5"> | |
| 324 | + <a-form-item :label="index === 0 ? '状态' : ''"> | |
| 325 | + <a-select v-model:value="period.isOpen"> | |
| 326 | + <a-select-option :value="1">启用</a-select-option> | |
| 327 | + <a-select-option :value="0">关闭</a-select-option> | |
| 328 | + </a-select> | |
| 329 | + </a-form-item> | |
| 330 | + </a-col> | |
| 331 | + <a-col :span="4"> | |
| 332 | + <a-form-item :label="index === 0 ? '操作' : ''"> | |
| 333 | + <a-button danger block @click="removeTimePeriod(index)">删除</a-button> | |
| 334 | + </a-form-item> | |
| 335 | + </a-col> | |
| 336 | + </a-row> | |
| 337 | + </div> | |
| 338 | + <a-empty v-else description="暂无时段附加费配置" /> | |
| 339 | + | |
| 340 | + <a-divider orientation="left">预计送达与展示</a-divider> | |
| 341 | + <a-row :gutter="16"> | |
| 342 | + <a-col :span="12"> | |
| 343 | + <a-form-item label="预计送达基础时间(分钟)"> | |
| 344 | + <a-input-number v-model:value="config.distanceBasicTime" :min="0" style="width:100%" /> | |
| 345 | + </a-form-item> | |
| 346 | + </a-col> | |
| 347 | + <a-col :span="12"> | |
| 348 | + <a-form-item label="超出每km增加时间(分钟)"> | |
| 349 | + <a-input-number v-model:value="config.distanceMoreTime" :min="0" style="width:100%" /> | |
| 350 | + </a-form-item> | |
| 351 | + </a-col> | |
| 352 | + </a-row> | |
| 353 | + <a-form-item label="附近骑手显示范围(km)"> | |
| 354 | + <a-input-number v-model:value="config.riderDistance" :min="0" :step="0.1" style="width:100%" /> | |
| 355 | + </a-form-item> | |
| 356 | + </a-form> | |
| 357 | + </template> | |
| 358 | + <div v-else class="plan-empty-state"> | |
| 359 | + <a-empty description="当前租户还没有配送费方案"> | |
| 360 | + <template #extra> | |
| 361 | + <a-space> | |
| 362 | + <a-button type="primary" @click="initializeDefaultPlan" :loading="planSaving">初始化默认方案</a-button> | |
| 363 | + <a-button @click="createPlan" :loading="planSaving">新增空白方案</a-button> | |
| 364 | + </a-space> | |
| 365 | + </template> | |
| 366 | + </a-empty> | |
| 367 | + </div> | |
| 368 | + </div> | |
| 369 | + </div> | |
| 205 | 370 | </a-modal> |
| 206 | 371 | |
| 207 | 372 | <a-modal v-model:open="levelVisible" :title="`骑手等级配置 - ${levelCityName}`" width="900px" :footer="null"> |
| ... | ... | @@ -234,8 +399,7 @@ |
| 234 | 399 | </a-table> |
| 235 | 400 | </a-modal> |
| 236 | 401 | |
| 237 | - <a-modal v-model:open="levelEditVisible" :title="levelEditingId ? '编辑骑手等级' : '新增骑手等级'" | |
| 238 | - @ok="handleSaveLevel" :confirmLoading="levelSaving"> | |
| 402 | + <a-modal v-model:open="levelEditVisible" :title="levelEditingId ? '编辑骑手等级' : '新增骑手等级'" @ok="handleSaveLevel" :confirmLoading="levelSaving"> | |
| 239 | 403 | <a-form :model="levelForm" layout="vertical"> |
| 240 | 404 | <a-form-item label="等级编号"> |
| 241 | 405 | <a-input-number v-model:value="levelForm.levelId" :min="1" style="width:100%" /> |
| ... | ... | @@ -295,7 +459,7 @@ |
| 295 | 459 | </template> |
| 296 | 460 | |
| 297 | 461 | <script setup lang="ts"> |
| 298 | -import { ref, reactive, onMounted } from 'vue' | |
| 462 | +import { onMounted, reactive, ref } from 'vue' | |
| 299 | 463 | import { message } from 'ant-design-vue' |
| 300 | 464 | import { cityApi, riderLevelApi } from '@/api' |
| 301 | 465 | |
| ... | ... | @@ -306,6 +470,18 @@ type TimePeriodForm = { |
| 306 | 470 | money: number | null |
| 307 | 471 | } |
| 308 | 472 | |
| 473 | +type DistanceStepForm = { | |
| 474 | + endDistance: number | null | |
| 475 | + unitDistance: number | null | |
| 476 | + unitFee: number | null | |
| 477 | +} | |
| 478 | + | |
| 479 | +type PieceRuleForm = { | |
| 480 | + startPiece: number | null | |
| 481 | + endPiece: number | null | |
| 482 | + fee: number | null | |
| 483 | +} | |
| 484 | + | |
| 309 | 485 | const loading = ref(false) |
| 310 | 486 | const saving = ref(false) |
| 311 | 487 | const list = ref<any[]>([]) |
| ... | ... | @@ -315,11 +491,32 @@ const levelVisible = ref(false) |
| 315 | 491 | const levelEditVisible = ref(false) |
| 316 | 492 | const editingId = ref<number | null>(null) |
| 317 | 493 | const currentCityId = ref<number>(0) |
| 494 | +const currentCityName = ref('') | |
| 318 | 495 | const levelCityId = ref<number>(0) |
| 319 | 496 | const levelCityName = ref('') |
| 320 | 497 | const form = reactive({ name: '', areaCode: '', rate: 0, listOrder: 0 }) |
| 498 | + | |
| 321 | 499 | const config = ref<any>(null) |
| 500 | +const planList = ref<any[]>([]) | |
| 501 | +const planLoading = ref(false) | |
| 502 | +const planSaving = ref(false) | |
| 503 | +const selectedPlanId = ref<number | null>(null) | |
| 504 | +const currentPlan = ref<any>(null) | |
| 322 | 505 | const timePeriods = ref<TimePeriodForm[]>([]) |
| 506 | +const distanceSteps = ref<DistanceStepForm[]>([]) | |
| 507 | +const pieceRules = ref<PieceRuleForm[]>([]) | |
| 508 | +const previewing = ref(false) | |
| 509 | +const previewResult = ref<any>(null) | |
| 510 | +const previewForm = reactive({ | |
| 511 | + startLng: '', | |
| 512 | + startLat: '', | |
| 513 | + endLng: '', | |
| 514 | + endLat: '', | |
| 515 | + weight: 0, | |
| 516 | + pieces: 1, | |
| 517 | + serviceTime: '', | |
| 518 | +}) | |
| 519 | + | |
| 323 | 520 | const levelLoading = ref(false) |
| 324 | 521 | const levelSaving = ref(false) |
| 325 | 522 | const levelList = ref<any[]>([]) |
| ... | ... | @@ -367,7 +564,7 @@ async function loadList() { |
| 367 | 564 | loading.value = true |
| 368 | 565 | try { |
| 369 | 566 | const res: any = await cityApi.tree() |
| 370 | - list.value = res.data // 现在直接是一级列表 | |
| 567 | + list.value = res.data | |
| 371 | 568 | } finally { |
| 372 | 569 | loading.value = false |
| 373 | 570 | } |
| ... | ... | @@ -385,13 +582,6 @@ function openEdit(record: any) { |
| 385 | 582 | modalVisible.value = true |
| 386 | 583 | } |
| 387 | 584 | |
| 388 | -function openLevelConfig(record: any) { | |
| 389 | - levelCityId.value = record.id | |
| 390 | - levelCityName.value = record.name | |
| 391 | - levelVisible.value = true | |
| 392 | - loadLevels() | |
| 393 | -} | |
| 394 | - | |
| 395 | 585 | async function handleSave() { |
| 396 | 586 | saving.value = true |
| 397 | 587 | try { |
| ... | ... | @@ -416,34 +606,198 @@ async function toggleStatus(record: any) { |
| 416 | 606 | |
| 417 | 607 | async function openConfig(record: any) { |
| 418 | 608 | currentCityId.value = record.id |
| 419 | - const res: any = await cityApi.getConfig(record.id) | |
| 420 | - config.value = normalizeConfig(res.data) | |
| 609 | + currentCityName.value = record.name | |
| 610 | + configVisible.value = true | |
| 611 | + previewResult.value = null | |
| 612 | + Object.assign(previewForm, { startLng: '', startLat: '', endLng: '', endLat: '', weight: 0, pieces: 1, serviceTime: '' }) | |
| 613 | + await loadPlanList(record.id) | |
| 614 | +} | |
| 615 | + | |
| 616 | +async function loadPlanList(cityId: number, preferPlanId?: number) { | |
| 617 | + planLoading.value = true | |
| 618 | + try { | |
| 619 | + const res: any = await cityApi.listFeePlans(cityId) | |
| 620 | + planList.value = Array.isArray(res?.data) ? res.data : [] | |
| 621 | + const targetPlanId = preferPlanId || planList.value.find(item => item.isDefault === 1)?.id || planList.value[0]?.id | |
| 622 | + if (targetPlanId) { | |
| 623 | + await selectPlan(targetPlanId) | |
| 624 | + } else { | |
| 625 | + selectedPlanId.value = null | |
| 626 | + currentPlan.value = null | |
| 627 | + config.value = null | |
| 628 | + } | |
| 629 | + } finally { | |
| 630 | + planLoading.value = false | |
| 631 | + } | |
| 632 | +} | |
| 633 | + | |
| 634 | +async function selectPlan(planId: number) { | |
| 635 | + selectedPlanId.value = planId | |
| 636 | + const res: any = await cityApi.getFeePlan(currentCityId.value, planId) | |
| 637 | + const detail = res?.data || {} | |
| 638 | + currentPlan.value = { | |
| 639 | + id: detail.id, | |
| 640 | + name: detail.name || '', | |
| 641 | + status: detail.status ?? 1, | |
| 642 | + listOrder: detail.listOrder ?? 0, | |
| 643 | + remark: detail.remark || '', | |
| 644 | + isDefault: detail.isDefault ?? 0, | |
| 645 | + } | |
| 646 | + config.value = normalizeConfig(detail.config) | |
| 647 | + distanceSteps.value = (config.value.type6.distanceSteps || []).map((step: any) => ({ | |
| 648 | + endDistance: step.endDistance ?? 0, | |
| 649 | + unitDistance: step.unitDistance ?? 1, | |
| 650 | + unitFee: step.unitFee ?? 0, | |
| 651 | + })) | |
| 652 | + pieceRules.value = (config.value.type6.pieceRules || []).map((rule: any) => ({ | |
| 653 | + startPiece: rule.startPiece ?? 0, | |
| 654 | + endPiece: rule.endPiece ?? 0, | |
| 655 | + fee: rule.fee ?? 0, | |
| 656 | + })) | |
| 421 | 657 | timePeriods.value = (config.value.type6.times || []).map((period: any) => ({ |
| 422 | 658 | startText: minuteToText(period.start), |
| 423 | 659 | endText: minuteToText(period.end), |
| 424 | 660 | isOpen: period.isOpen === 0 ? 0 : 1, |
| 425 | 661 | money: period.money ?? 0, |
| 426 | 662 | })) |
| 427 | - configVisible.value = true | |
| 663 | + previewResult.value = null | |
| 664 | +} | |
| 665 | + | |
| 666 | +async function createPlan() { | |
| 667 | + if (!currentCityId.value) return | |
| 668 | + planSaving.value = true | |
| 669 | + try { | |
| 670 | + const payload = { | |
| 671 | + name: `方案${planList.value.length + 1}`, | |
| 672 | + status: 1, | |
| 673 | + listOrder: planList.value.length, | |
| 674 | + remark: '', | |
| 675 | + config: deepClone(config.value || normalizeConfig(null)), | |
| 676 | + } | |
| 677 | + const res: any = await cityApi.createFeePlan(currentCityId.value, payload) | |
| 678 | + message.success('方案已创建') | |
| 679 | + await loadPlanList(currentCityId.value, res?.data) | |
| 680 | + } catch (err: any) { | |
| 681 | + message.error(err?.message || '方案创建失败') | |
| 682 | + } finally { | |
| 683 | + planSaving.value = false | |
| 684 | + } | |
| 428 | 685 | } |
| 429 | 686 | |
| 430 | -async function handleConfigSave() { | |
| 687 | +async function initializeDefaultPlan() { | |
| 688 | + if (!currentCityId.value) return | |
| 689 | + planSaving.value = true | |
| 431 | 690 | try { |
| 432 | - config.value.type = [6] | |
| 433 | - config.value.type6.times = buildTimesPayload() | |
| 691 | + const res: any = await cityApi.initDefaultFeePlan(currentCityId.value) | |
| 692 | + message.success('默认方案已初始化') | |
| 693 | + await loadPlanList(currentCityId.value, res?.data) | |
| 434 | 694 | } catch (err: any) { |
| 435 | - message.error(err.message || '时段配置有误') | |
| 695 | + message.error(err?.message || '默认方案初始化失败') | |
| 696 | + } finally { | |
| 697 | + planSaving.value = false | |
| 698 | + } | |
| 699 | +} | |
| 700 | + | |
| 701 | +async function copyPlan() { | |
| 702 | + if (!selectedPlanId.value) return | |
| 703 | + const res: any = await cityApi.copyFeePlan(currentCityId.value, selectedPlanId.value) | |
| 704 | + message.success('方案已复制') | |
| 705 | + await loadPlanList(currentCityId.value, res?.data) | |
| 706 | +} | |
| 707 | + | |
| 708 | +async function deletePlan() { | |
| 709 | + if (!selectedPlanId.value) return | |
| 710 | + await cityApi.deleteFeePlan(currentCityId.value, selectedPlanId.value) | |
| 711 | + message.success('方案已删除') | |
| 712 | + await loadPlanList(currentCityId.value) | |
| 713 | +} | |
| 714 | + | |
| 715 | +async function setDefaultPlan() { | |
| 716 | + if (!selectedPlanId.value) return | |
| 717 | + if (currentPlan.value?.status !== 1) { | |
| 718 | + message.error('请先启用当前方案,再设为默认') | |
| 436 | 719 | return |
| 437 | 720 | } |
| 721 | + await cityApi.setDefaultFeePlan(currentCityId.value, selectedPlanId.value) | |
| 722 | + message.success('默认方案已更新') | |
| 723 | + await loadPlanList(currentCityId.value, selectedPlanId.value) | |
| 724 | +} | |
| 438 | 725 | |
| 439 | - saving.value = true | |
| 726 | +async function saveCurrentPlan() { | |
| 727 | + if (!selectedPlanId.value || !currentPlan.value) return | |
| 440 | 728 | try { |
| 441 | - await cityApi.updateConfig(currentCityId.value, config.value) | |
| 442 | - message.success('配置保存成功') | |
| 443 | - configVisible.value = false | |
| 729 | + const payload = buildPlanPayload() | |
| 730 | + planSaving.value = true | |
| 731 | + await cityApi.updateFeePlan(currentCityId.value, selectedPlanId.value, payload) | |
| 732 | + message.success('方案保存成功') | |
| 733 | + await loadPlanList(currentCityId.value, selectedPlanId.value) | |
| 734 | + } catch (err: any) { | |
| 735 | + message.error(err?.message || '方案保存失败') | |
| 444 | 736 | } finally { |
| 445 | - saving.value = false | |
| 737 | + planSaving.value = false | |
| 738 | + } | |
| 739 | +} | |
| 740 | + | |
| 741 | +async function previewPlan() { | |
| 742 | + try { | |
| 743 | + const payload = buildPlanPayload() | |
| 744 | + if (!previewForm.startLng || !previewForm.startLat || !previewForm.endLng || !previewForm.endLat) { | |
| 745 | + message.error('请填写完整的试算经纬度') | |
| 746 | + return | |
| 747 | + } | |
| 748 | + previewing.value = true | |
| 749 | + const res: any = await cityApi.previewFeePlan(currentCityId.value, { | |
| 750 | + config: payload.config, | |
| 751 | + calc: { | |
| 752 | + startLng: previewForm.startLng, | |
| 753 | + startLat: previewForm.startLat, | |
| 754 | + endLng: previewForm.endLng, | |
| 755 | + endLat: previewForm.endLat, | |
| 756 | + weight: previewForm.weight ?? 0, | |
| 757 | + pieces: previewForm.pieces ?? 0, | |
| 758 | + serviceTime: previewForm.serviceTime ? Number(previewForm.serviceTime) : 0, | |
| 759 | + }, | |
| 760 | + }) | |
| 761 | + previewResult.value = res?.data || null | |
| 762 | + } catch (err: any) { | |
| 763 | + message.error(err?.message || '试算失败') | |
| 764 | + } finally { | |
| 765 | + previewing.value = false | |
| 766 | + } | |
| 767 | +} | |
| 768 | + | |
| 769 | +function buildPlanPayload() { | |
| 770 | + if (!currentPlan.value) { | |
| 771 | + throw new Error('请选择计价方案') | |
| 772 | + } | |
| 773 | + const planName = String(currentPlan.value.name || '').trim() | |
| 774 | + if (!planName) { | |
| 775 | + throw new Error('请填写方案名称') | |
| 446 | 776 | } |
| 777 | + const nextConfig = deepClone(config.value || normalizeConfig(null)) | |
| 778 | + nextConfig.type = [6] | |
| 779 | + nextConfig.type6.baseSwitch = nextConfig.type6.baseFee > 0 ? 1 : 0 | |
| 780 | + nextConfig.type6.distanceSwitch = 1 | |
| 781 | + nextConfig.type6.weightSwitch = | |
| 782 | + nextConfig.type6.weightFirst > 0 || nextConfig.type6.weightFirstFee > 0 || nextConfig.type6.weightUnitFee > 0 ? 1 : 0 | |
| 783 | + nextConfig.type6.pieceSwitch = pieceRules.value.length ? 1 : 0 | |
| 784 | + nextConfig.type6.distanceSteps = buildDistanceStepsPayload() | |
| 785 | + nextConfig.type6.pieceRules = buildPieceRulesPayload() | |
| 786 | + nextConfig.type6.times = buildTimesPayload() | |
| 787 | + return { | |
| 788 | + name: planName, | |
| 789 | + status: currentPlan.value.status ?? 1, | |
| 790 | + listOrder: currentPlan.value.listOrder ?? 0, | |
| 791 | + remark: currentPlan.value.remark || '', | |
| 792 | + config: nextConfig, | |
| 793 | + } | |
| 794 | +} | |
| 795 | + | |
| 796 | +function openLevelConfig(record: any) { | |
| 797 | + levelCityId.value = record.id | |
| 798 | + levelCityName.value = record.name | |
| 799 | + levelVisible.value = true | |
| 800 | + loadLevels() | |
| 447 | 801 | } |
| 448 | 802 | |
| 449 | 803 | async function loadLevels() { |
| ... | ... | @@ -537,18 +891,28 @@ function formatLevelRule(record: any) { |
| 537 | 891 | |
| 538 | 892 | function createDefaultType6() { |
| 539 | 893 | return { |
| 540 | - feeMode: 1, | |
| 894 | + minFee: 0, | |
| 895 | + baseSwitch: 0, | |
| 896 | + baseFee: 0, | |
| 897 | + feeMode: 2, | |
| 541 | 898 | fixMoney: 0, |
| 542 | 899 | distanceSwitch: 1, |
| 543 | 900 | distanceBasic: 3, |
| 544 | 901 | distanceBasicMoney: 4, |
| 545 | 902 | distanceMoreMoney: 1.5, |
| 546 | 903 | distanceType: 1, |
| 547 | - weightSwitch: 0, | |
| 904 | + distanceSteps: [], | |
| 905 | + weightSwitch: 1, | |
| 906 | + weightFirst: 5, | |
| 907 | + weightFirstFee: 0, | |
| 908 | + weightUnitFee: 1, | |
| 909 | + weightCapFee: 30, | |
| 548 | 910 | weightBasic: 0, |
| 549 | 911 | weightBasicMoney: 0, |
| 550 | 912 | weightMoreMoney: 0, |
| 551 | 913 | weightType: 1, |
| 914 | + pieceSwitch: 0, | |
| 915 | + pieceRules: [], | |
| 552 | 916 | times: [], |
| 553 | 917 | } |
| 554 | 918 | } |
| ... | ... | @@ -561,6 +925,8 @@ function normalizeConfig(raw: any) { |
| 561 | 925 | type: [6], |
| 562 | 926 | type6: { |
| 563 | 927 | ...type6, |
| 928 | + distanceSteps: Array.isArray(type6.distanceSteps) ? type6.distanceSteps : [], | |
| 929 | + pieceRules: Array.isArray(type6.pieceRules) ? type6.pieceRules : [], | |
| 564 | 930 | times: Array.isArray(type6.times) ? type6.times : [], |
| 565 | 931 | }, |
| 566 | 932 | distanceBasic: next.distanceBasic ?? 3, |
| ... | ... | @@ -571,30 +937,76 @@ function normalizeConfig(raw: any) { |
| 571 | 937 | } |
| 572 | 938 | |
| 573 | 939 | function addTimePeriod() { |
| 574 | - timePeriods.value.push({ | |
| 575 | - startText: '', | |
| 576 | - endText: '', | |
| 577 | - isOpen: 1, | |
| 578 | - money: 0, | |
| 579 | - }) | |
| 940 | + timePeriods.value.push({ startText: '', endText: '', isOpen: 1, money: 0 }) | |
| 580 | 941 | } |
| 581 | 942 | |
| 582 | 943 | function removeTimePeriod(index: number) { |
| 583 | 944 | timePeriods.value.splice(index, 1) |
| 584 | 945 | } |
| 585 | 946 | |
| 947 | +function addDistanceStep() { | |
| 948 | + distanceSteps.value.push({ endDistance: 0, unitDistance: 1, unitFee: 0 }) | |
| 949 | +} | |
| 950 | + | |
| 951 | +function removeDistanceStep(index: number) { | |
| 952 | + distanceSteps.value.splice(index, 1) | |
| 953 | +} | |
| 954 | + | |
| 955 | +function addPieceRule() { | |
| 956 | + pieceRules.value.push({ startPiece: 0, endPiece: 0, fee: 0 }) | |
| 957 | +} | |
| 958 | + | |
| 959 | +function removePieceRule(index: number) { | |
| 960 | + pieceRules.value.splice(index, 1) | |
| 961 | +} | |
| 962 | + | |
| 963 | +function buildDistanceStepsPayload() { | |
| 964 | + let prevEnd = config.value.type6.distanceBasic ?? 0 | |
| 965 | + return distanceSteps.value.map((step, index) => { | |
| 966 | + const endDistance = step.endDistance ?? 0 | |
| 967 | + const unitDistance = step.unitDistance ?? 0 | |
| 968 | + const unitFee = step.unitFee ?? 0 | |
| 969 | + if (endDistance <= prevEnd) { | |
| 970 | + throw new Error(`第${index + 1}条里程阶梯结束里程必须大于上一阶梯`) | |
| 971 | + } | |
| 972 | + if (unitDistance <= 0) { | |
| 973 | + throw new Error(`第${index + 1}条里程阶梯每档里程必须大于0`) | |
| 974 | + } | |
| 975 | + prevEnd = endDistance | |
| 976 | + return { endDistance, unitDistance, unitFee, listOrder: index } | |
| 977 | + }) | |
| 978 | +} | |
| 979 | + | |
| 980 | +function buildPieceRulesPayload() { | |
| 981 | + const payload = pieceRules.value | |
| 982 | + .map((rule, index) => { | |
| 983 | + const startPiece = rule.startPiece ?? 0 | |
| 984 | + const endPiece = rule.endPiece ?? 0 | |
| 985 | + if (startPiece > endPiece) { | |
| 986 | + throw new Error(`第${index + 1}条件数区间起始值不能大于结束值`) | |
| 987 | + } | |
| 988 | + return { startPiece, endPiece, fee: rule.fee ?? 0, listOrder: index } | |
| 989 | + }) | |
| 990 | + .sort((a, b) => a.startPiece - b.startPiece) | |
| 991 | + | |
| 992 | + for (let index = 1; index < payload.length; index += 1) { | |
| 993 | + if (payload[index].startPiece <= payload[index - 1].endPiece) { | |
| 994 | + throw new Error('件数区间不能重叠') | |
| 995 | + } | |
| 996 | + } | |
| 997 | + return payload | |
| 998 | +} | |
| 999 | + | |
| 586 | 1000 | function buildTimesPayload() { |
| 587 | 1001 | return timePeriods.value |
| 588 | 1002 | .map((period, index) => { |
| 589 | 1003 | const hasContent = period.startText || period.endText || period.money |
| 590 | 1004 | if (!hasContent) return null |
| 591 | - | |
| 592 | 1005 | const start = textToMinute(period.startText, `第${index + 1}条时段开始时间格式错误`) |
| 593 | 1006 | const end = textToMinute(period.endText, `第${index + 1}条时段结束时间格式错误`) |
| 594 | - if (start >= end) { | |
| 595 | - throw new Error(`第${index + 1}条时段结束时间必须晚于开始时间`) | |
| 1007 | + if (start === end) { | |
| 1008 | + throw new Error(`第${index + 1}条时段开始时间不能等于结束时间`) | |
| 596 | 1009 | } |
| 597 | - | |
| 598 | 1010 | return { |
| 599 | 1011 | start, |
| 600 | 1012 | end, |
| ... | ... | @@ -603,7 +1015,6 @@ function buildTimesPayload() { |
| 603 | 1015 | } |
| 604 | 1016 | }) |
| 605 | 1017 | .filter(Boolean) |
| 606 | - .sort((a: any, b: any) => a.start - b.start) | |
| 607 | 1018 | } |
| 608 | 1019 | |
| 609 | 1020 | function textToMinute(text: string, errorMessage: string) { |
| ... | ... | @@ -621,5 +1032,130 @@ function minuteToText(value: number | null | undefined) { |
| 621 | 1032 | return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}` |
| 622 | 1033 | } |
| 623 | 1034 | |
| 1035 | +function deepClone<T>(value: T): T { | |
| 1036 | + return JSON.parse(JSON.stringify(value)) | |
| 1037 | +} | |
| 1038 | + | |
| 624 | 1039 | onMounted(loadList) |
| 625 | 1040 | </script> |
| 1041 | + | |
| 1042 | +<style scoped> | |
| 1043 | +.plan-layout { | |
| 1044 | + display: flex; | |
| 1045 | + gap: 20px; | |
| 1046 | + min-height: 720px; | |
| 1047 | +} | |
| 1048 | + | |
| 1049 | +.plan-sidebar { | |
| 1050 | + width: 280px; | |
| 1051 | + flex-shrink: 0; | |
| 1052 | + display: flex; | |
| 1053 | + flex-direction: column; | |
| 1054 | + border-radius: 16px; | |
| 1055 | + background: #f7f8fc; | |
| 1056 | + padding: 16px; | |
| 1057 | +} | |
| 1058 | + | |
| 1059 | +.plan-sidebar-header { | |
| 1060 | + display: flex; | |
| 1061 | + align-items: flex-start; | |
| 1062 | + justify-content: space-between; | |
| 1063 | + gap: 12px; | |
| 1064 | + margin-bottom: 16px; | |
| 1065 | +} | |
| 1066 | + | |
| 1067 | +.plan-sidebar-title { | |
| 1068 | + font-size: 16px; | |
| 1069 | + font-weight: 600; | |
| 1070 | + color: #1f2430; | |
| 1071 | +} | |
| 1072 | + | |
| 1073 | +.plan-sidebar-subtitle { | |
| 1074 | + margin-top: 4px; | |
| 1075 | + font-size: 12px; | |
| 1076 | + color: #7a8091; | |
| 1077 | + line-height: 1.5; | |
| 1078 | +} | |
| 1079 | + | |
| 1080 | +.plan-list { | |
| 1081 | + display: flex; | |
| 1082 | + flex-direction: column; | |
| 1083 | + gap: 12px; | |
| 1084 | + max-height: 640px; | |
| 1085 | + overflow-y: auto; | |
| 1086 | + padding-right: 4px; | |
| 1087 | +} | |
| 1088 | + | |
| 1089 | +.plan-item { | |
| 1090 | + width: 100%; | |
| 1091 | + border: 0; | |
| 1092 | + border-radius: 14px; | |
| 1093 | + background: #fff; | |
| 1094 | + padding: 14px; | |
| 1095 | + text-align: left; | |
| 1096 | + cursor: pointer; | |
| 1097 | + box-shadow: 0 8px 20px rgba(31, 36, 48, 0.06); | |
| 1098 | + transition: all 0.2s ease; | |
| 1099 | +} | |
| 1100 | + | |
| 1101 | +.plan-item.active { | |
| 1102 | + background: #eef2ff; | |
| 1103 | + box-shadow: 0 10px 24px rgba(99, 102, 241, 0.18); | |
| 1104 | +} | |
| 1105 | + | |
| 1106 | +.plan-item-top { | |
| 1107 | + display: flex; | |
| 1108 | + align-items: center; | |
| 1109 | + justify-content: space-between; | |
| 1110 | + gap: 8px; | |
| 1111 | + margin-bottom: 8px; | |
| 1112 | +} | |
| 1113 | + | |
| 1114 | +.plan-item-name { | |
| 1115 | + font-size: 14px; | |
| 1116 | + font-weight: 600; | |
| 1117 | + color: #1f2430; | |
| 1118 | +} | |
| 1119 | + | |
| 1120 | +.plan-item-bottom { | |
| 1121 | + display: flex; | |
| 1122 | + justify-content: space-between; | |
| 1123 | + font-size: 12px; | |
| 1124 | + color: #7a8091; | |
| 1125 | +} | |
| 1126 | + | |
| 1127 | +.plan-content { | |
| 1128 | + flex: 1; | |
| 1129 | + min-width: 0; | |
| 1130 | + max-height: 720px; | |
| 1131 | + overflow-y: auto; | |
| 1132 | + padding-right: 4px; | |
| 1133 | +} | |
| 1134 | + | |
| 1135 | +.plan-toolbar { | |
| 1136 | + display: flex; | |
| 1137 | + align-items: center; | |
| 1138 | + justify-content: space-between; | |
| 1139 | + gap: 12px; | |
| 1140 | + margin-bottom: 16px; | |
| 1141 | +} | |
| 1142 | + | |
| 1143 | +.preview-card { | |
| 1144 | + margin-bottom: 20px; | |
| 1145 | + border-radius: 16px; | |
| 1146 | + background: #fafbff; | |
| 1147 | +} | |
| 1148 | + | |
| 1149 | +.preview-result { | |
| 1150 | + display: flex; | |
| 1151 | + flex-wrap: wrap; | |
| 1152 | + gap: 8px; | |
| 1153 | +} | |
| 1154 | + | |
| 1155 | +.plan-empty-state { | |
| 1156 | + min-height: 520px; | |
| 1157 | + display: flex; | |
| 1158 | + align-items: center; | |
| 1159 | + justify-content: center; | |
| 1160 | +} | |
| 1161 | +</style> | ... | ... |
src/views/dashboard/DashboardHome.vue
| ... | ... | @@ -65,15 +65,15 @@ function go(path: string) { |
| 65 | 65 | <style scoped> |
| 66 | 66 | .dashboard-home { |
| 67 | 67 | display: grid; |
| 68 | - gap: 22px; | |
| 68 | + gap: 16px; | |
| 69 | 69 | } |
| 70 | 70 | |
| 71 | 71 | .hero-card { |
| 72 | 72 | display: grid; |
| 73 | 73 | grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.9fr); |
| 74 | - gap: 18px; | |
| 75 | - border-radius: 34px; | |
| 76 | - padding: 28px; | |
| 74 | + gap: 14px; | |
| 75 | + border-radius: 24px; | |
| 76 | + padding: 20px; | |
| 77 | 77 | background: |
| 78 | 78 | linear-gradient(160deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.62)), |
| 79 | 79 | linear-gradient(135deg, rgba(198, 185, 255, 0.72), rgba(255, 217, 236, 0.78)); |
| ... | ... | @@ -83,7 +83,7 @@ function go(path: string) { |
| 83 | 83 | |
| 84 | 84 | .hero-pill { |
| 85 | 85 | display: inline-flex; |
| 86 | - padding: 8px 14px; | |
| 86 | + padding: 6px 12px; | |
| 87 | 87 | border-radius: 999px; |
| 88 | 88 | background: rgba(255, 255, 255, 0.76); |
| 89 | 89 | color: #6f5fe2; |
| ... | ... | @@ -94,9 +94,9 @@ function go(path: string) { |
| 94 | 94 | } |
| 95 | 95 | |
| 96 | 96 | .hero-copy h2 { |
| 97 | - margin: 16px 0 10px; | |
| 98 | - font-size: 34px; | |
| 99 | - line-height: 1.08; | |
| 97 | + margin: 12px 0 8px; | |
| 98 | + font-size: 28px; | |
| 99 | + line-height: 1.15; | |
| 100 | 100 | font-family: 'Outfit', sans-serif; |
| 101 | 101 | color: #2f2946; |
| 102 | 102 | } |
| ... | ... | @@ -110,16 +110,16 @@ function go(path: string) { |
| 110 | 110 | |
| 111 | 111 | .hero-grid { |
| 112 | 112 | display: flex; |
| 113 | - gap: 14px; | |
| 113 | + gap: 12px; | |
| 114 | 114 | flex-wrap: wrap; |
| 115 | - margin-top: 24px; | |
| 115 | + margin-top: 18px; | |
| 116 | 116 | } |
| 117 | 117 | |
| 118 | 118 | .hero-metric { |
| 119 | 119 | min-width: 190px; |
| 120 | - border-radius: 22px; | |
| 120 | + border-radius: 18px; | |
| 121 | 121 | background: rgba(255, 255, 255, 0.74); |
| 122 | - padding: 14px 16px; | |
| 122 | + padding: 12px 14px; | |
| 123 | 123 | } |
| 124 | 124 | |
| 125 | 125 | .hero-metric span { |
| ... | ... | @@ -141,7 +141,7 @@ function go(path: string) { |
| 141 | 141 | display: flex; |
| 142 | 142 | align-items: flex-end; |
| 143 | 143 | justify-content: center; |
| 144 | - min-height: 260px; | |
| 144 | + min-height: 220px; | |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | 147 | .hero-visual img { |
| ... | ... | @@ -175,21 +175,21 @@ function go(path: string) { |
| 175 | 175 | .content-grid { |
| 176 | 176 | display: grid; |
| 177 | 177 | grid-template-columns: 1.2fr 0.8fr; |
| 178 | - gap: 18px; | |
| 178 | + gap: 14px; | |
| 179 | 179 | } |
| 180 | 180 | |
| 181 | 181 | .quick-links { |
| 182 | 182 | display: grid; |
| 183 | 183 | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| 184 | - gap: 14px; | |
| 184 | + gap: 12px; | |
| 185 | 185 | } |
| 186 | 186 | |
| 187 | 187 | .quick-link { |
| 188 | 188 | text-align: left; |
| 189 | 189 | border: 1px solid rgba(202, 193, 240, 0.34); |
| 190 | 190 | background: rgba(255, 255, 255, 0.78); |
| 191 | - border-radius: 24px; | |
| 192 | - padding: 18px; | |
| 191 | + border-radius: 18px; | |
| 192 | + padding: 14px; | |
| 193 | 193 | cursor: pointer; |
| 194 | 194 | } |
| 195 | 195 | |
| ... | ... | @@ -210,7 +210,7 @@ function go(path: string) { |
| 210 | 210 | } |
| 211 | 211 | |
| 212 | 212 | .hero-copy h2 { |
| 213 | - font-size: 28px; | |
| 213 | + font-size: 24px; | |
| 214 | 214 | } |
| 215 | 215 | } |
| 216 | 216 | </style> | ... | ... |
src/views/open/OpenMockDelivery.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <div class="mock-delivery-page"> | |
| 3 | + <a-card title="模拟推送配送单" :bordered="false"> | |
| 4 | + <template #extra> | |
| 5 | + <a-space> | |
| 6 | + <a-button @click="fillDemo">填充示例</a-button> | |
| 7 | + <a-button @click="resetForm">重置</a-button> | |
| 8 | + <a-button :loading="calcLoading" @click="handleCalcFee">按应用租户试算配送费</a-button> | |
| 9 | + <a-button type="primary" :loading="saving" @click="handleSubmit">发送推单</a-button> | |
| 10 | + </a-space> | |
| 11 | + </template> | |
| 12 | + | |
| 13 | + <a-form layout="vertical"> | |
| 14 | + <a-row :gutter="16"> | |
| 15 | + <a-col :xs="24" :lg="12"> | |
| 16 | + <a-form-item label="开放应用"> | |
| 17 | + <a-select | |
| 18 | + v-model:value="form.appId" | |
| 19 | + placeholder="请选择应用" | |
| 20 | + show-search | |
| 21 | + :filter-option="filterAppOption" | |
| 22 | + @change="handleAppChange" | |
| 23 | + > | |
| 24 | + <a-select-option v-for="item in appList" :key="item.id" :value="item.id"> | |
| 25 | + {{ item.appName }}({{ item.appKey }}) | |
| 26 | + </a-select-option> | |
| 27 | + </a-select> | |
| 28 | + </a-form-item> | |
| 29 | + </a-col> | |
| 30 | + <a-col :xs="24" :lg="12"> | |
| 31 | + <a-form-item label="所属租户"> | |
| 32 | + <a-input :value="selectedTenantName" disabled /> | |
| 33 | + </a-form-item> | |
| 34 | + </a-col> | |
| 35 | + </a-row> | |
| 36 | + | |
| 37 | + <a-row :gutter="16"> | |
| 38 | + <a-col :xs="24" :lg="12"> | |
| 39 | + <a-form-item label="外部订单号"> | |
| 40 | + <a-input v-model:value="form.outOrderNo" placeholder="请输入外部订单号" /> | |
| 41 | + </a-form-item> | |
| 42 | + </a-col> | |
| 43 | + <a-col :xs="24" :lg="12"> | |
| 44 | + <a-form-item label="期望配送时间戳(秒)"> | |
| 45 | + <a-input-number v-model:value="form.serviceTime" :min="0" style="width:100%" /> | |
| 46 | + </a-form-item> | |
| 47 | + </a-col> | |
| 48 | + </a-row> | |
| 49 | + | |
| 50 | + <a-divider orientation="left">发货方信息</a-divider> | |
| 51 | + <a-alert | |
| 52 | + type="info" | |
| 53 | + show-icon | |
| 54 | + message="如果已同步过门店,优先填写接入方门店编号;未填写时请手动补全发货门店名称、地址和经纬度。" | |
| 55 | + style="margin-bottom: 16px" | |
| 56 | + /> | |
| 57 | + <a-row :gutter="16"> | |
| 58 | + <a-col :xs="24" :lg="12"> | |
| 59 | + <a-form-item label="接入方门店编号"> | |
| 60 | + <a-input v-model:value="form.outStoreId" placeholder="可选,填写后可自动补门店信息" /> | |
| 61 | + </a-form-item> | |
| 62 | + </a-col> | |
| 63 | + <a-col :xs="24" :lg="12"> | |
| 64 | + <a-form-item label="发货门店名称"> | |
| 65 | + <a-input v-model:value="form.storeName" placeholder="未填写门店编号时必填" /> | |
| 66 | + </a-form-item> | |
| 67 | + </a-col> | |
| 68 | + </a-row> | |
| 69 | + <a-form-item label="发货门店地址"> | |
| 70 | + <a-input v-model:value="form.storeAddr" placeholder="请输入发货门店地址" /> | |
| 71 | + </a-form-item> | |
| 72 | + <a-row :gutter="16"> | |
| 73 | + <a-col :xs="24" :lg="12"> | |
| 74 | + <a-form-item label="发货经度"> | |
| 75 | + <a-input v-model:value="form.storeLng" placeholder="如:113.264385" /> | |
| 76 | + </a-form-item> | |
| 77 | + </a-col> | |
| 78 | + <a-col :xs="24" :lg="12"> | |
| 79 | + <a-form-item label="发货纬度"> | |
| 80 | + <a-input v-model:value="form.storeLat" placeholder="如:23.129112" /> | |
| 81 | + </a-form-item> | |
| 82 | + </a-col> | |
| 83 | + </a-row> | |
| 84 | + | |
| 85 | + <a-divider orientation="left">收件人信息</a-divider> | |
| 86 | + <a-row :gutter="16"> | |
| 87 | + <a-col :xs="24" :lg="12"> | |
| 88 | + <a-form-item label="收件人姓名"> | |
| 89 | + <a-input v-model:value="form.recipName" placeholder="请输入收件人姓名" /> | |
| 90 | + </a-form-item> | |
| 91 | + </a-col> | |
| 92 | + <a-col :xs="24" :lg="12"> | |
| 93 | + <a-form-item label="收件人电话"> | |
| 94 | + <a-input v-model:value="form.recipPhone" placeholder="请输入收件人手机号" /> | |
| 95 | + </a-form-item> | |
| 96 | + </a-col> | |
| 97 | + </a-row> | |
| 98 | + <a-form-item label="收件人地址"> | |
| 99 | + <a-input v-model:value="form.recipAddr" placeholder="请输入收件地址" /> | |
| 100 | + </a-form-item> | |
| 101 | + <a-row :gutter="16"> | |
| 102 | + <a-col :xs="24" :lg="12"> | |
| 103 | + <a-form-item label="收件经度"> | |
| 104 | + <a-input v-model:value="form.recipLng" placeholder="如:113.270000" /> | |
| 105 | + </a-form-item> | |
| 106 | + </a-col> | |
| 107 | + <a-col :xs="24" :lg="12"> | |
| 108 | + <a-form-item label="收件纬度"> | |
| 109 | + <a-input v-model:value="form.recipLat" placeholder="如:23.135000" /> | |
| 110 | + </a-form-item> | |
| 111 | + </a-col> | |
| 112 | + </a-row> | |
| 113 | + | |
| 114 | + <a-divider orientation="left">订单信息</a-divider> | |
| 115 | + <a-row :gutter="16"> | |
| 116 | + <a-col :xs="24" :lg="12"> | |
| 117 | + <a-form-item label="货物重量(kg)"> | |
| 118 | + <a-input-number v-model:value="form.weight" :min="0" :step="0.1" style="width:100%" /> | |
| 119 | + </a-form-item> | |
| 120 | + </a-col> | |
| 121 | + <a-col :xs="24" :lg="12"> | |
| 122 | + <a-form-item label="订单级回调地址"> | |
| 123 | + <a-input v-model:value="form.callbackUrl" placeholder="可选,留空则使用应用级 Webhook" /> | |
| 124 | + </a-form-item> | |
| 125 | + </a-col> | |
| 126 | + </a-row> | |
| 127 | + <a-form-item label="整单备注"> | |
| 128 | + <a-textarea v-model:value="form.remark" :rows="3" placeholder="如:请尽快送达" /> | |
| 129 | + </a-form-item> | |
| 130 | + <a-form-item label="货物备注"> | |
| 131 | + <a-textarea v-model:value="form.itemRemark" :rows="2" placeholder="如:饮品分开装、轻拿轻放" /> | |
| 132 | + </a-form-item> | |
| 133 | + | |
| 134 | + <a-divider orientation="left">货物清单</a-divider> | |
| 135 | + <div style="margin-bottom:12px"> | |
| 136 | + <a-button type="dashed" block @click="addItem">新增货物</a-button> | |
| 137 | + </div> | |
| 138 | + <div v-if="form.items.length"> | |
| 139 | + <a-row | |
| 140 | + v-for="(item, index) in form.items" | |
| 141 | + :key="index" | |
| 142 | + :gutter="12" | |
| 143 | + style="margin-bottom:12px;align-items:flex-start" | |
| 144 | + > | |
| 145 | + <a-col :xs="24" :lg="6"> | |
| 146 | + <a-form-item :label="index === 0 ? '名称' : ''"> | |
| 147 | + <a-input v-model:value="item.name" placeholder="如:招牌套餐" /> | |
| 148 | + </a-form-item> | |
| 149 | + </a-col> | |
| 150 | + <a-col :xs="24" :lg="4"> | |
| 151 | + <a-form-item :label="index === 0 ? '数量' : ''"> | |
| 152 | + <a-input-number v-model:value="item.quantity" :min="1" style="width:100%" /> | |
| 153 | + </a-form-item> | |
| 154 | + </a-col> | |
| 155 | + <a-col :xs="24" :lg="6"> | |
| 156 | + <a-form-item :label="index === 0 ? '规格' : ''"> | |
| 157 | + <a-input v-model:value="item.spec" placeholder="如:大杯 / 双拼" /> | |
| 158 | + </a-form-item> | |
| 159 | + </a-col> | |
| 160 | + <a-col :xs="24" :lg="6"> | |
| 161 | + <a-form-item :label="index === 0 ? '备注' : ''"> | |
| 162 | + <a-input v-model:value="item.remark" placeholder="可选" /> | |
| 163 | + </a-form-item> | |
| 164 | + </a-col> | |
| 165 | + <a-col :xs="24" :lg="2"> | |
| 166 | + <a-form-item :label="index === 0 ? '操作' : ''"> | |
| 167 | + <a-button danger block @click="removeItem(index)">删除</a-button> | |
| 168 | + </a-form-item> | |
| 169 | + </a-col> | |
| 170 | + </a-row> | |
| 171 | + </div> | |
| 172 | + <a-empty v-else description="暂无货物清单" /> | |
| 173 | + </a-form> | |
| 174 | + </a-card> | |
| 175 | + | |
| 176 | + <a-card v-if="feeResult" title="按应用租户试算结果" :bordered="false"> | |
| 177 | + <a-alert | |
| 178 | + type="info" | |
| 179 | + show-icon | |
| 180 | + message="试算时不会手填租户,系统会自动使用当前开放应用绑定的租户;实际推单金额以后端创建订单时实时计算为准。" | |
| 181 | + style="margin-bottom: 16px" | |
| 182 | + /> | |
| 183 | + <a-descriptions bordered :column="2"> | |
| 184 | + <a-descriptions-item label="配送费">¥{{ feeResult.totalFee }}</a-descriptions-item> | |
| 185 | + <a-descriptions-item label="配送距离">{{ feeResult.distance }} km</a-descriptions-item> | |
| 186 | + <a-descriptions-item label="预计送达">{{ feeResult.estimatedMinutes }} 分钟</a-descriptions-item> | |
| 187 | + <a-descriptions-item label="基础费用"> | |
| 188 | + ¥{{ feeResult.moneyBasic }}{{ feeResult.moneyBasicTxt ? ` ${feeResult.moneyBasicTxt}` : '' }} | |
| 189 | + </a-descriptions-item> | |
| 190 | + <a-descriptions-item label="超距费用"> | |
| 191 | + ¥{{ feeResult.moneyDistance }}{{ feeResult.moneyDistanceTxt ? ` ${feeResult.moneyDistanceTxt}` : '' }} | |
| 192 | + </a-descriptions-item> | |
| 193 | + <a-descriptions-item label="超重费用"> | |
| 194 | + ¥{{ feeResult.moneyWeight }}{{ feeResult.moneyWeightTxt ? ` ${feeResult.moneyWeightTxt}` : '' }} | |
| 195 | + </a-descriptions-item> | |
| 196 | + <a-descriptions-item label="件数费用"> | |
| 197 | + ¥{{ feeResult.moneyPiece }}{{ feeResult.moneyPieceTxt ? ` ${feeResult.moneyPieceTxt}` : '' }} | |
| 198 | + </a-descriptions-item> | |
| 199 | + <a-descriptions-item label="时段附加费">¥{{ feeResult.moneyTime }}</a-descriptions-item> | |
| 200 | + <a-descriptions-item label="保底费用">¥{{ feeResult.minFee }}{{ feeResult.minFeeApplied === 1 ? '(已命中)' : '' }}</a-descriptions-item> | |
| 201 | + <a-descriptions-item label="计费重量">{{ feeResult.weight }} kg</a-descriptions-item> | |
| 202 | + <a-descriptions-item label="计费件数">{{ feeResult.pieces || 0 }} 件</a-descriptions-item> | |
| 203 | + </a-descriptions> | |
| 204 | + </a-card> | |
| 205 | + | |
| 206 | + <a-card v-if="result" title="推单结果" :bordered="false"> | |
| 207 | + <a-descriptions bordered :column="2"> | |
| 208 | + <a-descriptions-item label="配送单ID">{{ result.deliveryOrderId }}</a-descriptions-item> | |
| 209 | + <a-descriptions-item label="平台订单号">{{ result.orderNo }}</a-descriptions-item> | |
| 210 | + <a-descriptions-item label="外部订单号">{{ result.outOrderNo }}</a-descriptions-item> | |
| 211 | + <a-descriptions-item label="状态">{{ statusMap[result.status] || result.status }}</a-descriptions-item> | |
| 212 | + <a-descriptions-item label="配送费">¥{{ result.totalFee }}</a-descriptions-item> | |
| 213 | + <a-descriptions-item label="配送距离">{{ result.distance }} km</a-descriptions-item> | |
| 214 | + <a-descriptions-item label="基础费">¥{{ result.moneyBasic }}</a-descriptions-item> | |
| 215 | + <a-descriptions-item label="超距费">¥{{ result.moneyDistance }}</a-descriptions-item> | |
| 216 | + <a-descriptions-item label="时段附加费">¥{{ result.moneyTime }}</a-descriptions-item> | |
| 217 | + <a-descriptions-item label="预计送达">{{ result.estimatedMinutes }} 分钟</a-descriptions-item> | |
| 218 | + </a-descriptions> | |
| 219 | + </a-card> | |
| 220 | + </div> | |
| 221 | +</template> | |
| 222 | + | |
| 223 | +<script setup lang="ts"> | |
| 224 | +import { computed, onMounted, reactive, ref, watch } from 'vue' | |
| 225 | +import { message } from 'ant-design-vue' | |
| 226 | +import { cityApi, deliveryApi, openApi } from '@/api' | |
| 227 | + | |
| 228 | +type OpenAppItem = { | |
| 229 | + id: number | |
| 230 | + appName: string | |
| 231 | + appKey: string | |
| 232 | + cityId: number | |
| 233 | + status: number | |
| 234 | +} | |
| 235 | + | |
| 236 | +type DeliveryItem = { | |
| 237 | + name: string | |
| 238 | + quantity: number | |
| 239 | + spec: string | |
| 240 | + remark: string | |
| 241 | +} | |
| 242 | + | |
| 243 | +const appList = ref<OpenAppItem[]>([]) | |
| 244 | +const cityList = ref<any[]>([]) | |
| 245 | +const saving = ref(false) | |
| 246 | +const calcLoading = ref(false) | |
| 247 | +const feeResult = ref<any>(null) | |
| 248 | +const result = ref<any>(null) | |
| 249 | + | |
| 250 | +const statusMap: Record<number, string> = { | |
| 251 | + 2: '待接单', | |
| 252 | + 3: '已接单', | |
| 253 | + 4: '配送中', | |
| 254 | + 6: '已完成', | |
| 255 | + 10: '已取消', | |
| 256 | +} | |
| 257 | + | |
| 258 | +function createDefaultItem(): DeliveryItem { | |
| 259 | + return { name: '', quantity: 1, spec: '', remark: '' } | |
| 260 | +} | |
| 261 | + | |
| 262 | +function createDefaultForm() { | |
| 263 | + return { | |
| 264 | + appId: undefined as number | undefined, | |
| 265 | + outStoreId: '', | |
| 266 | + storeName: '', | |
| 267 | + storeAddr: '', | |
| 268 | + storeLng: '', | |
| 269 | + storeLat: '', | |
| 270 | + recipName: '', | |
| 271 | + recipPhone: '', | |
| 272 | + recipAddr: '', | |
| 273 | + recipLng: '', | |
| 274 | + recipLat: '', | |
| 275 | + outOrderNo: generateOutOrderNo(), | |
| 276 | + weight: 0, | |
| 277 | + serviceTime: 0, | |
| 278 | + callbackUrl: '', | |
| 279 | + remark: '', | |
| 280 | + itemRemark: '', | |
| 281 | + items: [createDefaultItem()], | |
| 282 | + } | |
| 283 | +} | |
| 284 | + | |
| 285 | +const form = reactive(createDefaultForm()) | |
| 286 | + | |
| 287 | +const selectedApp = computed(() => appList.value.find(item => item.id === form.appId)) | |
| 288 | +const selectedTenantName = computed(() => { | |
| 289 | + const city = cityList.value.find(item => item.id === selectedApp.value?.cityId) | |
| 290 | + return city?.name || '' | |
| 291 | +}) | |
| 292 | + | |
| 293 | +function generateOutOrderNo() { | |
| 294 | + return `MOCK${Date.now()}` | |
| 295 | +} | |
| 296 | + | |
| 297 | +function resetForm() { | |
| 298 | + Object.assign(form, createDefaultForm()) | |
| 299 | + feeResult.value = null | |
| 300 | + result.value = null | |
| 301 | +} | |
| 302 | + | |
| 303 | +function fillDemo() { | |
| 304 | + if (!form.appId && appList.value.length) { | |
| 305 | + form.appId = appList.value[0].id | |
| 306 | + } | |
| 307 | + Object.assign(form, { | |
| 308 | + ...form, | |
| 309 | + outOrderNo: generateOutOrderNo(), | |
| 310 | + outStoreId: '', | |
| 311 | + storeName: '测试门店', | |
| 312 | + storeAddr: '广州市天河区体育西路 100 号', | |
| 313 | + storeLng: '113.327823', | |
| 314 | + storeLat: '23.137466', | |
| 315 | + recipName: '测试用户', | |
| 316 | + recipPhone: '13800138000', | |
| 317 | + recipAddr: '广州市天河区珠江新城测试地址', | |
| 318 | + recipLng: '113.324590', | |
| 319 | + recipLat: '23.119295', | |
| 320 | + weight: 1, | |
| 321 | + serviceTime: 0, | |
| 322 | + callbackUrl: '', | |
| 323 | + remark: '后台模拟推单', | |
| 324 | + itemRemark: '少冰少辣', | |
| 325 | + }) | |
| 326 | + form.items = [ | |
| 327 | + { name: '招牌奶茶', quantity: 2, spec: '大杯', remark: '少冰' }, | |
| 328 | + { name: '鸡腿饭', quantity: 1, spec: '标准', remark: '不要辣' }, | |
| 329 | + ] | |
| 330 | + feeResult.value = null | |
| 331 | + result.value = null | |
| 332 | +} | |
| 333 | + | |
| 334 | +function addItem() { | |
| 335 | + form.items.push(createDefaultItem()) | |
| 336 | +} | |
| 337 | + | |
| 338 | +function removeItem(index: number) { | |
| 339 | + form.items.splice(index, 1) | |
| 340 | + if (!form.items.length) { | |
| 341 | + form.items.push(createDefaultItem()) | |
| 342 | + } | |
| 343 | +} | |
| 344 | + | |
| 345 | +function handleAppChange() { | |
| 346 | + feeResult.value = null | |
| 347 | + result.value = null | |
| 348 | +} | |
| 349 | + | |
| 350 | +function filterAppOption(input: string, option: any) { | |
| 351 | + return String(option.children).toLowerCase().includes(input.toLowerCase()) | |
| 352 | +} | |
| 353 | + | |
| 354 | +function buildPayload() { | |
| 355 | + return { | |
| 356 | + appId: form.appId, | |
| 357 | + outStoreId: form.outStoreId || undefined, | |
| 358 | + storeName: form.storeName || undefined, | |
| 359 | + storeAddr: form.storeAddr || undefined, | |
| 360 | + storeLng: form.storeLng || undefined, | |
| 361 | + storeLat: form.storeLat || undefined, | |
| 362 | + recipName: form.recipName, | |
| 363 | + recipPhone: form.recipPhone, | |
| 364 | + recipAddr: form.recipAddr, | |
| 365 | + recipLng: form.recipLng, | |
| 366 | + recipLat: form.recipLat, | |
| 367 | + outOrderNo: form.outOrderNo, | |
| 368 | + weight: form.weight, | |
| 369 | + serviceTime: form.serviceTime, | |
| 370 | + callbackUrl: form.callbackUrl || undefined, | |
| 371 | + remark: form.remark || undefined, | |
| 372 | + itemRemark: form.itemRemark || undefined, | |
| 373 | + items: form.items | |
| 374 | + .filter(item => item.name.trim()) | |
| 375 | + .map(item => ({ | |
| 376 | + name: item.name.trim(), | |
| 377 | + quantity: item.quantity || 1, | |
| 378 | + spec: item.spec || undefined, | |
| 379 | + remark: item.remark || undefined, | |
| 380 | + })), | |
| 381 | + } | |
| 382 | +} | |
| 383 | + | |
| 384 | +function clearCalcResult() { | |
| 385 | + feeResult.value = null | |
| 386 | +} | |
| 387 | + | |
| 388 | +function buildCalcPayload() { | |
| 389 | + return { | |
| 390 | + cityId: selectedApp.value?.cityId, | |
| 391 | + orderType: 6, | |
| 392 | + startLng: form.storeLng, | |
| 393 | + startLat: form.storeLat, | |
| 394 | + endLng: form.recipLng, | |
| 395 | + endLat: form.recipLat, | |
| 396 | + weight: form.weight, | |
| 397 | + pieces: form.items.reduce((sum, item) => sum + (item.quantity || 0), 0), | |
| 398 | + serviceTime: form.serviceTime, | |
| 399 | + } | |
| 400 | +} | |
| 401 | + | |
| 402 | +async function loadApps() { | |
| 403 | + const res: any = await openApi.list() | |
| 404 | + appList.value = Array.isArray(res?.data) ? res.data.filter((item: OpenAppItem) => item.status === 1) : [] | |
| 405 | +} | |
| 406 | + | |
| 407 | +async function loadCities() { | |
| 408 | + const res: any = await cityApi.openList() | |
| 409 | + cityList.value = Array.isArray(res?.data) ? res.data : [] | |
| 410 | +} | |
| 411 | + | |
| 412 | +async function handleSubmit() { | |
| 413 | + if (!form.appId) { | |
| 414 | + message.error('请选择开放应用') | |
| 415 | + return | |
| 416 | + } | |
| 417 | + if (!form.outOrderNo || !form.recipName || !form.recipPhone || !form.recipAddr || !form.recipLng || !form.recipLat) { | |
| 418 | + message.error('请填写完整的收件人与订单信息') | |
| 419 | + return | |
| 420 | + } | |
| 421 | + if (!form.outStoreId && (!form.storeName || !form.storeLng || !form.storeLat)) { | |
| 422 | + message.error('未填写门店编号时,请补全发货门店名称和经纬度') | |
| 423 | + return | |
| 424 | + } | |
| 425 | + | |
| 426 | + saving.value = true | |
| 427 | + try { | |
| 428 | + const res: any = await openApi.mockDeliveryCreate(buildPayload()) | |
| 429 | + result.value = res.data | |
| 430 | + message.success('模拟推单成功') | |
| 431 | + } finally { | |
| 432 | + saving.value = false | |
| 433 | + } | |
| 434 | +} | |
| 435 | + | |
| 436 | +async function handleCalcFee() { | |
| 437 | + if (!form.appId || !selectedApp.value?.cityId) { | |
| 438 | + message.error('请选择开放应用') | |
| 439 | + return | |
| 440 | + } | |
| 441 | + if (!form.storeLng || !form.storeLat || !form.recipLng || !form.recipLat) { | |
| 442 | + message.error('请填写完整的发货与收货经纬度') | |
| 443 | + return | |
| 444 | + } | |
| 445 | + | |
| 446 | + calcLoading.value = true | |
| 447 | + try { | |
| 448 | + const res: any = await deliveryApi.calc(buildCalcPayload()) | |
| 449 | + feeResult.value = res.data | |
| 450 | + message.success('配送费计算完成') | |
| 451 | + } finally { | |
| 452 | + calcLoading.value = false | |
| 453 | + } | |
| 454 | +} | |
| 455 | + | |
| 456 | +onMounted(async () => { | |
| 457 | + await Promise.all([loadApps(), loadCities()]) | |
| 458 | + if (appList.value.length) { | |
| 459 | + form.appId = appList.value[0].id | |
| 460 | + } | |
| 461 | +}) | |
| 462 | + | |
| 463 | +watch( | |
| 464 | + () => [form.storeLng, form.storeLat, form.recipLng, form.recipLat, form.weight, form.serviceTime, form.outStoreId], | |
| 465 | + () => { | |
| 466 | + clearCalcResult() | |
| 467 | + } | |
| 468 | +) | |
| 469 | +</script> | |
| 470 | + | |
| 471 | +<style scoped> | |
| 472 | +.mock-delivery-page { | |
| 473 | + display: grid; | |
| 474 | + gap: 18px; | |
| 475 | +} | |
| 476 | +</style> | ... | ... |