Commit b37151b626a60e4d06c8403d211396fb3a3aada7

Authored by 杨刚
1 parent 054f8469

init

src/api/index.ts
... ... @@ -35,7 +35,7 @@ export const substationApi = {
35 35 ban: (id: number) => request.post('/api/platform/substation/ban', null, { params: { id } }),
36 36 cancelBan: (id: number) => request.post('/api/platform/substation/cancelBan', null, { params: { id } }),
37 37 del: (id: number) => request.delete('/api/platform/substation/del', { params: { id } }),
38   - changePassword: (data: { oldPassword: string; newPassword: string }) =>
  38 + changePassword: (data: { id: number; oldPassword: string; newPassword: string }) =>
39 39 request.post('/api/platform/substation/changePassword', data),
40 40 }
41 41  
... ...
src/layouts/MainLayout.vue
1 1 <template>
2   - <div class="layout-shell">
  2 + <div class="layout-shell" :class="{ collapsed }">
3 3 <aside class="soft-sider" :class="{ collapsed }">
4 4 <button class="sider-toggle" type="button" @click="collapsed = !collapsed">
5 5 <menu-fold-outlined v-if="!collapsed" />
... ... @@ -18,6 +18,7 @@
18 18 <a-menu
19 19 v-model:selectedKeys="selectedKeys"
20 20 mode="inline"
  21 + :inline-collapsed="collapsed"
21 22 @click="onMenuClick"
22 23 >
23 24 <a-menu-item key="/dashboard">
... ... @@ -163,6 +164,10 @@ function handleLogout() {
163 164 padding: 16px;
164 165 }
165 166  
  167 +.layout-shell.collapsed {
  168 + grid-template-columns: 88px minmax(0, 1fr);
  169 +}
  170 +
166 171 .soft-sider,
167 172 .soft-topbar {
168 173 border: 1px solid rgba(255, 255, 255, 0.58);
... ... @@ -190,6 +195,10 @@ function handleLogout() {
190 195 padding-right: 4px;
191 196 }
192 197  
  198 +.soft-sider.collapsed .menu-scroll {
  199 + padding-right: 0;
  200 +}
  201 +
193 202 .menu-scroll::-webkit-scrollbar {
194 203 width: 8px;
195 204 }
... ... @@ -201,6 +210,7 @@ function handleLogout() {
201 210  
202 211 .soft-sider.collapsed {
203 212 width: 88px;
  213 + padding-inline: 8px;
204 214 }
205 215  
206 216 .sider-toggle {
... ... @@ -215,6 +225,10 @@ function handleLogout() {
215 225 margin-bottom: 12px;
216 226 }
217 227  
  228 +.soft-sider.collapsed .sider-toggle {
  229 + align-self: center;
  230 +}
  231 +
218 232 .brand-block {
219 233 display: flex;
220 234 align-items: center;
... ... @@ -222,6 +236,11 @@ function handleLogout() {
222 236 padding: 6px 8px 14px;
223 237 }
224 238  
  239 +.soft-sider.collapsed .brand-block {
  240 + justify-content: center;
  241 + padding-inline: 0;
  242 +}
  243 +
225 244 .brand-mark {
226 245 width: 44px;
227 246 height: 44px;
... ... @@ -274,6 +293,10 @@ h1 {
274 293 background: transparent;
275 294 }
276 295  
  296 +.soft-sider.collapsed :deep(.ant-menu) {
  297 + width: 100%;
  298 +}
  299 +
277 300 .sider-foot {
278 301 margin-top: 10px;
279 302 flex-shrink: 0;
... ... @@ -384,6 +407,33 @@ h1 {
384 407 font-size: 13px;
385 408 }
386 409  
  410 +.soft-sider.collapsed :deep(.ant-menu-item),
  411 +.soft-sider.collapsed :deep(.ant-menu-submenu-title) {
  412 + width: 100% !important;
  413 + margin-inline: 0 !important;
  414 + padding-inline: 0 !important;
  415 + display: flex !important;
  416 + align-items: center !important;
  417 + justify-content: center !important;
  418 + text-align: center;
  419 +}
  420 +
  421 +.soft-sider.collapsed :deep(.ant-menu-inline-collapsed .ant-menu-item .ant-menu-item-icon),
  422 +.soft-sider.collapsed :deep(.ant-menu-inline-collapsed .ant-menu-submenu-title .ant-menu-item-icon),
  423 +.soft-sider.collapsed :deep(.ant-menu-inline-collapsed .ant-menu-item .anticon),
  424 +.soft-sider.collapsed :deep(.ant-menu-inline-collapsed .ant-menu-submenu-title .anticon) {
  425 + margin-inline: 0;
  426 + font-size: 16px;
  427 +}
  428 +
  429 +.soft-sider.collapsed :deep(.ant-menu-title-content) {
  430 + display: none !important;
  431 +}
  432 +
  433 +.soft-sider.collapsed :deep(.ant-menu-submenu-arrow) {
  434 + display: none !important;
  435 +}
  436 +
387 437 @media (max-width: 960px) {
388 438 .layout-shell {
389 439 grid-template-columns: 1fr;
... ...
src/style.css
... ... @@ -226,6 +226,76 @@ a {
226 226  
227 227 .ant-modal .ant-modal-footer {
228 228 padding: 12px 20px 18px;
  229 + border-top: 1px solid rgba(189, 180, 234, 0.14);
  230 +}
  231 +
  232 +.ant-modal .ant-modal-footer .ant-btn + .ant-btn {
  233 + margin-inline-start: 8px;
  234 +}
  235 +
  236 +.ant-modal .ant-form-vertical .ant-form-item-label {
  237 + padding-bottom: 6px;
  238 +}
  239 +
  240 +.ant-modal .ant-form-vertical .ant-form-item-label > label {
  241 + color: var(--text-dark);
  242 + font-size: var(--font-size-md);
  243 + font-weight: 600;
  244 +}
  245 +
  246 +.ant-modal .ant-row {
  247 + row-gap: 0;
  248 +}
  249 +
  250 +.ant-modal .ant-radio-group {
  251 + display: inline-flex;
  252 + gap: 8px;
  253 + flex-wrap: wrap;
  254 +}
  255 +
  256 +.ant-modal .ant-radio-wrapper,
  257 +.ant-modal .ant-radio-button-wrapper {
  258 + font-size: var(--font-size-md);
  259 +}
  260 +
  261 +.ant-modal textarea.ant-input {
  262 + min-height: 88px;
  263 + line-height: 1.6;
  264 + padding-top: 8px;
  265 + padding-bottom: 8px;
  266 +}
  267 +
  268 +.ant-modal .ant-switch {
  269 + min-width: 38px;
  270 +}
  271 +
  272 +.ant-descriptions {
  273 + border-radius: 14px;
  274 + overflow: hidden;
  275 +}
  276 +
  277 +.ant-descriptions .ant-descriptions-item-label {
  278 + width: 128px;
  279 + background: rgba(244, 240, 255, 0.72) !important;
  280 + color: var(--text-dark) !important;
  281 + font-size: var(--font-size-md);
  282 + font-weight: 600;
  283 +}
  284 +
  285 +.ant-descriptions .ant-descriptions-item-content {
  286 + background: rgba(255, 255, 255, 0.72) !important;
  287 + font-size: var(--font-size-md);
  288 +}
  289 +
  290 +.ant-modal .ant-alert {
  291 + border-radius: 12px;
  292 + font-size: var(--font-size-md);
  293 +}
  294 +
  295 +.ant-modal .ant-table-small .ant-table-thead > tr > th,
  296 +.ant-modal .ant-table-small .ant-table-tbody > tr > td {
  297 + padding-top: 9px;
  298 + padding-bottom: 9px;
229 299 }
230 300  
231 301 .ant-tag {
... ... @@ -268,6 +338,30 @@ a {
268 338 color: var(--brand-deep) !important;
269 339 }
270 340  
  341 +.ant-menu-submenu-popup .ant-menu {
  342 + padding: 8px !important;
  343 + border-radius: 16px !important;
  344 + border: 1px solid rgba(228, 223, 247, 0.72) !important;
  345 + background: rgba(255, 255, 255, 0.96) !important;
  346 + backdrop-filter: blur(20px);
  347 + box-shadow: 0 18px 40px rgba(121, 104, 213, 0.14) !important;
  348 +}
  349 +
  350 +.ant-menu-submenu-popup .ant-menu-item,
  351 +.ant-menu-submenu-popup .ant-menu-submenu-title {
  352 + min-height: 36px !important;
  353 + line-height: 36px !important;
  354 + margin: 2px 0 !important;
  355 + width: 100% !important;
  356 + border-radius: 12px !important;
  357 + font-size: var(--font-size-md) !important;
  358 +}
  359 +
  360 +.ant-menu-submenu-popup .ant-menu-item:hover,
  361 +.ant-menu-submenu-popup .ant-menu-submenu-title:hover {
  362 + background: rgba(244, 240, 255, 0.92) !important;
  363 +}
  364 +
271 365 .ant-menu-item:hover,
272 366 .ant-menu-submenu-title:hover {
273 367 color: var(--brand-deep) !important;
... ... @@ -304,8 +398,165 @@ a {
304 398 pointer-events: none;
305 399 }
306 400  
  401 +.soft-page-stack {
  402 + display: grid;
  403 + gap: 16px;
  404 +}
  405 +
  406 +.soft-section {
  407 + display: grid;
  408 + gap: 14px;
  409 + margin-bottom: 18px;
  410 +}
  411 +
  412 +.soft-section:last-child {
  413 + margin-bottom: 0;
  414 +}
  415 +
  416 +.soft-section-header {
  417 + display: flex;
  418 + align-items: flex-start;
  419 + justify-content: space-between;
  420 + gap: 12px;
  421 + flex-wrap: wrap;
  422 +}
  423 +
  424 +.soft-section-heading {
  425 + min-width: 0;
  426 +}
  427 +
  428 +.soft-section-title {
  429 + margin: 0;
  430 + color: var(--text-dark);
  431 + font-size: 15px;
  432 + font-weight: 700;
  433 + line-height: 1.35;
  434 +}
  435 +
  436 +.soft-section-subtitle {
  437 + margin: 4px 0 0;
  438 + color: var(--text-soft);
  439 + font-size: var(--font-size-sm);
  440 + line-height: 1.6;
  441 +}
  442 +
  443 +.soft-section-actions {
  444 + display: flex;
  445 + align-items: center;
  446 + gap: 8px;
  447 + flex-wrap: wrap;
  448 +}
  449 +
  450 +.soft-note-card {
  451 + border-radius: 16px;
  452 + padding: 14px 16px;
  453 + background: linear-gradient(135deg, rgba(244, 240, 255, 0.9), rgba(255, 250, 253, 0.9));
  454 + border: 1px solid rgba(206, 196, 244, 0.36);
  455 +}
  456 +
  457 +.soft-note-card strong {
  458 + display: block;
  459 + margin-bottom: 4px;
  460 + color: var(--text-dark);
  461 + font-size: var(--font-size-md);
  462 +}
  463 +
  464 +.soft-note-card p {
  465 + margin: 0;
  466 + color: var(--text-soft);
  467 + font-size: var(--font-size-sm);
  468 + line-height: 1.6;
  469 +}
  470 +
  471 +.soft-inline-actions {
  472 + display: flex;
  473 + align-items: center;
  474 + gap: 10px;
  475 + flex-wrap: wrap;
  476 +}
  477 +
  478 +.soft-dashed-block {
  479 + border-radius: 18px;
  480 + border: 1px dashed rgba(180, 170, 231, 0.5);
  481 + background: rgba(255, 255, 255, 0.48);
  482 + padding: 14px;
  483 +}
  484 +
  485 +.soft-result-grid {
  486 + display: grid;
  487 + gap: 16px;
  488 +}
  489 +
  490 +.list-toolbar {
  491 + display: flex;
  492 + align-items: center;
  493 + justify-content: space-between;
  494 + gap: 12px;
  495 + margin-bottom: 14px;
  496 + flex-wrap: wrap;
  497 +}
  498 +
  499 +.list-toolbar-left,
  500 +.list-toolbar-right {
  501 + display: flex;
  502 + align-items: center;
  503 + gap: 10px;
  504 + flex-wrap: wrap;
  505 +}
  506 +
  507 +.list-toolbar-right {
  508 + justify-content: flex-end;
  509 +}
  510 +
  511 +.list-filter {
  512 + min-width: 120px;
  513 +}
  514 +
  515 +.list-search {
  516 + width: 220px;
  517 +}
  518 +
  519 +.list-table-card .ant-card-head {
  520 + min-height: 52px;
  521 +}
  522 +
  523 +.list-table-card .ant-card-head-title {
  524 + font-size: 15px;
  525 +}
  526 +
  527 +.list-table-card .ant-card-body {
  528 + padding-top: 16px;
  529 +}
  530 +
307 531 @media (max-width: 1200px) {
308 532 .soft-page-shell {
309 533 padding: 18px;
310 534 }
311 535 }
  536 +
  537 +@media (max-width: 960px) {
  538 + .soft-section-header {
  539 + align-items: stretch;
  540 + }
  541 +
  542 + .soft-section-actions {
  543 + width: 100%;
  544 + }
  545 +
  546 + .list-toolbar {
  547 + align-items: stretch;
  548 + }
  549 +
  550 + .list-toolbar-left,
  551 + .list-toolbar-right {
  552 + width: 100%;
  553 + }
  554 +
  555 + .list-toolbar-right {
  556 + justify-content: flex-start;
  557 + }
  558 +
  559 + .list-search {
  560 + width: 100%;
  561 + }
  562 +}
... ...
src/views/city/CityList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="租户管理" :bordered="false">
4   - <template #extra>
5   - <a-button type="primary" @click="openAdd">新增</a-button>
6   - </template>
  3 + <a-card title="租户管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-right">
  6 + <a-button type="primary" @click="openAdd">新增</a-button>
  7 + </div>
  8 + </div>
7 9 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
8 10 <template #bodyCell="{ column, record }">
9 11 <template v-if="column.key === 'status'">
... ... @@ -78,6 +80,11 @@
78 80  
79 81 <div class="plan-content">
80 82 <template v-if="currentPlan && config">
  83 + <div class="soft-note-card plan-note-card">
  84 + <strong>当前配置说明</strong>
  85 + <p>当前弹窗只维护外卖配送规则。你可以在同一租户下维护多套方案,并设置默认启用方案用于实时计价。</p>
  86 + </div>
  87 +
81 88 <div class="plan-toolbar">
82 89 <a-space wrap>
83 90 <a-button @click="copyPlan" :disabled="!selectedPlanId">复制当前方案</a-button>
... ... @@ -92,32 +99,42 @@
92 99 </a-space>
93 100 </div>
94 101  
95   - <a-row :gutter="16">
96   - <a-col :span="8">
97   - <a-form-item label="方案名称">
98   - <a-input v-model:value="currentPlan.name" placeholder="如:标准午高峰方案" />
99   - </a-form-item>
100   - </a-col>
101   - <a-col :span="8">
102   - <a-form-item label="状态">
103   - <a-select v-model:value="currentPlan.status">
104   - <a-select-option :value="1">启用</a-select-option>
105   - <a-select-option :value="0">停用</a-select-option>
106   - </a-select>
107   - </a-form-item>
108   - </a-col>
109   - <a-col :span="8">
110   - <a-form-item label="排序">
111   - <a-input-number v-model:value="currentPlan.listOrder" :min="0" style="width:100%" />
112   - </a-form-item>
113   - </a-col>
114   - </a-row>
115   - <a-form-item label="备注">
116   - <a-input v-model:value="currentPlan.remark" placeholder="可填写适用业务场景说明" />
117   - </a-form-item>
  102 + <div class="plan-section">
  103 + <div class="soft-section-header">
  104 + <div class="soft-section-heading">
  105 + <h3 class="soft-section-title">方案基础信息</h3>
  106 + <p class="soft-section-subtitle">维护方案名称、状态和备注说明。</p>
  107 + </div>
  108 + </div>
  109 +
  110 + <a-row :gutter="16">
  111 + <a-col :span="8">
  112 + <a-form-item label="方案名称">
  113 + <a-input v-model:value="currentPlan.name" placeholder="如:标准午高峰方案" />
  114 + </a-form-item>
  115 + </a-col>
  116 + <a-col :span="8">
  117 + <a-form-item label="状态">
  118 + <a-select v-model:value="currentPlan.status">
  119 + <a-select-option :value="1">启用</a-select-option>
  120 + <a-select-option :value="0">停用</a-select-option>
  121 + </a-select>
  122 + </a-form-item>
  123 + </a-col>
  124 + <a-col :span="8">
  125 + <a-form-item label="排序">
  126 + <a-input-number v-model:value="currentPlan.listOrder" :min="0" style="width:100%" />
  127 + </a-form-item>
  128 + </a-col>
  129 + </a-row>
  130 + <a-form-item label="备注">
  131 + <a-input v-model:value="currentPlan.remark" placeholder="可填写适用业务场景说明" />
  132 + </a-form-item>
  133 + </div>
118 134  
119 135 <a-card class="preview-card" :bordered="false">
120 136 <template #title>草稿试算</template>
  137 + <div class="soft-section-subtitle preview-subtitle">保存前可先按草稿参数试算配送费,确认距离、时段和附加项是否符合预期。</div>
121 138 <a-row :gutter="12">
122 139 <a-col :span="6">
123 140 <a-form-item label="起点经度">
... ... @@ -172,42 +189,55 @@
172 189 </a-card>
173 190  
174 191 <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>
  192 + <div class="plan-section">
  193 + <div class="soft-section-header">
  194 + <div class="soft-section-heading">
  195 + <h3 class="soft-section-title">费用总览</h3>
  196 + <p class="soft-section-subtitle">基础费与保底费会参与最终总价计算。</p>
  197 + </div>
  198 + </div>
  199 + <a-row :gutter="16">
  200 + <a-col :span="12">
  201 + <a-form-item label="保底费用(元)">
  202 + <a-input-number v-model:value="config.type6.minFee" :min="0" :step="0.1" style="width:100%" />
  203 + </a-form-item>
  204 + </a-col>
  205 + <a-col :span="12">
  206 + <a-form-item label="基础费(元/单)">
  207 + <a-input-number v-model:value="config.type6.baseFee" :min="0" :step="0.1" style="width:100%" />
  208 + </a-form-item>
  209 + </a-col>
  210 + </a-row>
204 211 </div>
205   - <div v-if="distanceSteps.length">
  212 +
  213 + <div class="plan-section">
  214 + <div class="soft-section-header">
  215 + <div class="soft-section-heading">
  216 + <h3 class="soft-section-title">里程阶梯</h3>
  217 + <p class="soft-section-subtitle">先配置起步距离,再追加超出后的阶梯规则。</p>
  218 + </div>
  219 + <div class="soft-section-actions">
  220 + <a-button type="dashed" @click="addDistanceStep">新增阶梯段</a-button>
  221 + </div>
  222 + </div>
  223 + <a-row :gutter="16">
  224 + <a-col :span="12">
  225 + <a-form-item label="起步里程(km内)">
  226 + <a-input-number v-model:value="config.type6.distanceBasic" :min="0" :step="0.1" style="width:100%" />
  227 + </a-form-item>
  228 + </a-col>
  229 + <a-col :span="12">
  230 + <a-form-item label="起步费用(元)">
  231 + <a-input-number v-model:value="config.type6.distanceBasicMoney" :min="0" :step="0.1" style="width:100%" />
  232 + </a-form-item>
  233 + </a-col>
  234 + </a-row>
  235 + <div v-if="distanceSteps.length" class="soft-dashed-block">
206 236 <a-row
207 237 v-for="(step, index) in distanceSteps"
208 238 :key="index"
209 239 :gutter="12"
210   - style="margin-bottom:12px;align-items:flex-start"
  240 + class="plan-dynamic-row"
211 241 >
212 242 <a-col :span="7">
213 243 <a-form-item :label="index === 0 ? '结束里程(km)' : ''">
... ... @@ -230,45 +260,59 @@
230 260 </a-form-item>
231 261 </a-col>
232 262 </a-row>
  263 + </div>
  264 + <a-empty v-else description="暂无里程阶梯配置" />
233 265 </div>
234   - <a-empty v-else description="暂无里程阶梯配置" />
235 266  
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>
  267 + <div class="plan-section">
  268 + <div class="soft-section-header">
  269 + <div class="soft-section-heading">
  270 + <h3 class="soft-section-title">重量计费</h3>
  271 + <p class="soft-section-subtitle">重量按首重和续重单价计算,可设置费用封顶。</p>
  272 + </div>
  273 + </div>
  274 + <a-row :gutter="16">
  275 + <a-col :span="12">
  276 + <a-form-item label="首重(kg)">
  277 + <a-input-number v-model:value="config.type6.weightFirst" :min="0" :step="0.1" style="width:100%" />
  278 + </a-form-item>
  279 + </a-col>
  280 + <a-col :span="12">
  281 + <a-form-item label="首重费用(元)">
  282 + <a-input-number v-model:value="config.type6.weightFirstFee" :min="0" :step="0.1" style="width:100%" />
  283 + </a-form-item>
  284 + </a-col>
  285 + </a-row>
  286 + <a-row :gutter="16">
  287 + <a-col :span="12">
  288 + <a-form-item label="续重单价(元/kg)">
  289 + <a-input-number v-model:value="config.type6.weightUnitFee" :min="0" :step="0.1" style="width:100%" />
  290 + </a-form-item>
  291 + </a-col>
  292 + <a-col :span="12">
  293 + <a-form-item label="封顶费用(元)">
  294 + <a-input-number v-model:value="config.type6.weightCapFee" :min="0" :step="0.1" style="width:100%" />
  295 + </a-form-item>
  296 + </a-col>
  297 + </a-row>
265 298 </div>
266   - <div v-if="pieceRules.length">
  299 +
  300 + <div class="plan-section">
  301 + <div class="soft-section-header">
  302 + <div class="soft-section-heading">
  303 + <h3 class="soft-section-title">件数计费</h3>
  304 + <p class="soft-section-subtitle">适合按商品件数额外加价的场景。</p>
  305 + </div>
  306 + <div class="soft-section-actions">
  307 + <a-button type="dashed" @click="addPieceRule">新增件数区间</a-button>
  308 + </div>
  309 + </div>
  310 + <div v-if="pieceRules.length" class="soft-dashed-block">
267 311 <a-row
268 312 v-for="(rule, index) in pieceRules"
269 313 :key="index"
270 314 :gutter="12"
271   - style="margin-bottom:12px;align-items:flex-start"
  315 + class="plan-dynamic-row"
272 316 >
273 317 <a-col :span="6">
274 318 <a-form-item :label="index === 0 ? '起始件数' : ''">
... ... @@ -291,19 +335,26 @@
291 335 </a-form-item>
292 336 </a-col>
293 337 </a-row>
  338 + </div>
  339 + <a-empty v-else description="暂无件数区间配置" />
294 340 </div>
295   - <a-empty v-else description="暂无件数区间配置" />
296 341  
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">
  342 + <div class="plan-section">
  343 + <div class="soft-section-header">
  344 + <div class="soft-section-heading">
  345 + <h3 class="soft-section-title">时段附加费</h3>
  346 + <p class="soft-section-subtitle">用于午晚高峰或夜间等时段加价。</p>
  347 + </div>
  348 + <div class="soft-section-actions">
  349 + <a-button type="dashed" @click="addTimePeriod">新增时段</a-button>
  350 + </div>
  351 + </div>
  352 + <div v-if="timePeriods.length" class="soft-dashed-block">
302 353 <a-row
303 354 v-for="(period, index) in timePeriods"
304 355 :key="index"
305 356 :gutter="12"
306   - style="margin-bottom:12px;align-items:flex-start"
  357 + class="plan-dynamic-row"
307 358 >
308 359 <a-col :span="5">
309 360 <a-form-item :label="index === 0 ? '开始时间' : ''">
... ... @@ -334,25 +385,33 @@
334 385 </a-form-item>
335 386 </a-col>
336 387 </a-row>
  388 + </div>
  389 + <a-empty v-else description="暂无时段附加费配置" />
337 390 </div>
338   - <a-empty v-else description="暂无时段附加费配置" />
339 391  
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>
  392 + <div class="plan-section plan-section-last">
  393 + <div class="soft-section-header">
  394 + <div class="soft-section-heading">
  395 + <h3 class="soft-section-title">预计送达与展示</h3>
  396 + <p class="soft-section-subtitle">控制预计送达时间和骑手可视距离。</p>
  397 + </div>
  398 + </div>
  399 + <a-row :gutter="16">
  400 + <a-col :span="12">
  401 + <a-form-item label="预计送达基础时间(分钟)">
  402 + <a-input-number v-model:value="config.distanceBasicTime" :min="0" style="width:100%" />
  403 + </a-form-item>
  404 + </a-col>
  405 + <a-col :span="12">
  406 + <a-form-item label="超出每km增加时间(分钟)">
  407 + <a-input-number v-model:value="config.distanceMoreTime" :min="0" style="width:100%" />
  408 + </a-form-item>
  409 + </a-col>
  410 + </a-row>
  411 + <a-form-item label="附近骑手显示范围(km)">
  412 + <a-input-number v-model:value="config.riderDistance" :min="0" :step="0.1" style="width:100%" />
  413 + </a-form-item>
  414 + </div>
356 415 </a-form>
357 416 </template>
358 417 <div v-else class="plan-empty-state">
... ... @@ -1132,6 +1191,10 @@ onMounted(loadList)
1132 1191 padding-right: 4px;
1133 1192 }
1134 1193  
  1194 +.plan-note-card {
  1195 + margin-bottom: 16px;
  1196 +}
  1197 +
1135 1198 .plan-toolbar {
1136 1199 display: flex;
1137 1200 align-items: center;
... ... @@ -1140,12 +1203,33 @@ onMounted(loadList)
1140 1203 margin-bottom: 16px;
1141 1204 }
1142 1205  
  1206 +.plan-section {
  1207 + margin-bottom: 22px;
  1208 + padding-bottom: 18px;
  1209 + border-bottom: 1px solid rgba(206, 196, 244, 0.2);
  1210 +}
  1211 +
  1212 +.plan-section-last {
  1213 + margin-bottom: 0;
  1214 + padding-bottom: 0;
  1215 + border-bottom: 0;
  1216 +}
  1217 +
  1218 +.plan-dynamic-row {
  1219 + margin-bottom: 12px;
  1220 + align-items: flex-start;
  1221 +}
  1222 +
1143 1223 .preview-card {
1144 1224 margin-bottom: 20px;
1145 1225 border-radius: 16px;
1146 1226 background: #fafbff;
1147 1227 }
1148 1228  
  1229 +.preview-subtitle {
  1230 + margin-bottom: 14px;
  1231 +}
  1232 +
1149 1233 .preview-result {
1150 1234 display: flex;
1151 1235 flex-wrap: wrap;
... ...
src/views/dashboard/DashboardHome.vue
1 1 <template>
2   - <div class="dashboard-home">
  2 + <div class="dashboard-home soft-page-stack">
3 3 <section class="hero-card">
4 4 <div class="hero-copy">
5   - <div class="hero-pill">Soft-Neo Dashboard</div>
  5 + <div class="hero-pill">运营总览</div>
6 6 <h2>欢迎回到地利外卖运营工作台</h2>
7   - <p>把租户、骑手、订单和分站管理集中在一张更轻盈的首页里,业务页面本身只保留导航与内容,让操作区域更大、更专注。</p>
  7 + <p>首页保留总览和快捷入口,业务菜单页只保留导航与内容区域,减少顶部公共模块对操作空间的挤压。</p>
8 8 <div class="hero-grid">
9 9 <div class="hero-metric">
10   - <span>Operation Focus</span>
11   - <strong>租户 · 骑手 · 订单</strong>
  10 + <span>核心范围</span>
  11 + <strong>租户 / 骑手 / 订单</strong>
12 12 </div>
13 13 <div class="hero-metric">
14   - <span>Visual Mood</span>
15   - <strong>Lavender / Warm Glow</strong>
  14 + <span>当前主题</span>
  15 + <strong>轻柔紫调 / 紧凑运营风格</strong>
16 16 </div>
17 17 </div>
18 18 </div>
... ... @@ -24,7 +24,13 @@
24 24 </section>
25 25  
26 26 <section class="content-grid">
27   - <a-card title="快捷入口" :bordered="false">
  27 + <a-card :bordered="false" class="soft-section-card">
  28 + <div class="soft-section-header">
  29 + <div class="soft-section-heading">
  30 + <h3 class="soft-section-title">快捷入口</h3>
  31 + <p class="soft-section-subtitle">常用运营菜单集中放在首页,减少跳转层级。</p>
  32 + </div>
  33 + </div>
28 34 <div class="quick-links">
29 35 <button v-for="item in quickLinks" :key="item.path" class="quick-link" type="button" @click="go(item.path)">
30 36 <strong>{{ item.title }}</strong>
... ... @@ -33,7 +39,13 @@
33 39 </div>
34 40 </a-card>
35 41  
36   - <a-card title="界面说明" :bordered="false">
  42 + <a-card :bordered="false" class="soft-section-card">
  43 + <div class="soft-section-header">
  44 + <div class="soft-section-heading">
  45 + <h3 class="soft-section-title">界面说明</h3>
  46 + <p class="soft-section-subtitle">这一版样式调整重点放在密度、统一性和中文后台习惯。</p>
  47 + </div>
  48 + </div>
37 49 <ul class="soft-notes">
38 50 <li>首页保留大视觉和信息卡片,适合做总览和快捷入口。</li>
39 51 <li>其他菜单页只保留紧凑头部和内容区,避免公共模块挤压表格空间。</li>
... ... @@ -95,12 +107,18 @@ function go(path: string) {
95 107  
96 108 .hero-copy h2 {
97 109 margin: 12px 0 8px;
98   - font-size: 28px;
  110 + font-size: 24px;
99 111 line-height: 1.15;
100 112 font-family: 'Outfit', sans-serif;
101 113 color: #2f2946;
102 114 }
103 115  
  116 +.hero-copy p {
  117 + margin: 0;
  118 + font-size: 13px;
  119 + line-height: 1.7;
  120 +}
  121 +
104 122 .hero-copy p,
105 123 .hero-metric span,
106 124 .quick-link span,
... ... @@ -119,7 +137,7 @@ function go(path: string) {
119 137 min-width: 190px;
120 138 border-radius: 18px;
121 139 background: rgba(255, 255, 255, 0.74);
122   - padding: 12px 14px;
  140 + padding: 11px 13px;
123 141 }
124 142  
125 143 .hero-metric span {
... ... @@ -193,9 +211,22 @@ function go(path: string) {
193 211 cursor: pointer;
194 212 }
195 213  
  214 +.quick-link strong {
  215 + display: block;
  216 + margin-bottom: 4px;
  217 + font-size: 14px;
  218 +}
  219 +
  220 +.quick-link span {
  221 + font-size: 12px;
  222 + line-height: 1.6;
  223 +}
  224 +
196 225 .soft-notes {
197 226 margin: 0;
198 227 padding-left: 18px;
  228 + font-size: 13px;
  229 + line-height: 1.7;
199 230 }
200 231  
201 232 .soft-notes li + li {
... ...
src/views/delivery/DeliveryOrderList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="配送订单" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-select v-model:value="filterStatus" placeholder="状态" allowClear style="width:130px" @change="loadList">
  3 + <a-card title="配送订单" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-select v-model:value="filterStatus" placeholder="状态" allowClear class="list-filter" @change="loadList">
7 7 <a-select-option :value="2">待接单</a-select-option>
8 8 <a-select-option :value="3">已接单</a-select-option>
9 9 <a-select-option :value="4">配送中</a-select-option>
10 10 <a-select-option :value="6">已完成</a-select-option>
11 11 <a-select-option :value="10">已取消</a-select-option>
12 12 </a-select>
13   - <a-input-search v-model:value="keyword" placeholder="外部订单号" @search="loadList" style="width:220px" />
  13 + <a-input-search v-model:value="keyword" placeholder="外部订单号" @search="loadList" class="list-search" />
  14 + </div>
  15 + <div class="list-toolbar-right">
14 16 <a-button type="primary" @click="queryByNo" :loading="querying">查询</a-button>
15   - </a-space>
16   - </template>
  17 + </div>
  18 + </div>
17 19 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
18 20 <template #bodyCell="{ column, record }">
19 21 <template v-if="column.key === 'status'">
... ... @@ -29,16 +31,21 @@
29 31 </a-table>
30 32 </a-card>
31 33  
32   - <!-- 查询结果弹窗 -->
33 34 <a-modal v-model:open="queryVisible" title="配送订单详情" :footer="null">
34   - <a-descriptions :column="1" bordered size="small" v-if="queryResult">
35   - <a-descriptions-item label="配送订单ID">{{ queryResult.deliveryOrderId }}</a-descriptions-item>
36   - <a-descriptions-item label="外部订单号">{{ queryResult.outOrderNo }}</a-descriptions-item>
37   - <a-descriptions-item label="状态">{{ statusMap[queryResult.status] }}</a-descriptions-item>
38   - <a-descriptions-item label="配送费">¥{{ queryResult.totalFee }}</a-descriptions-item>
39   - <a-descriptions-item label="距离">{{ queryResult.distance }} km</a-descriptions-item>
40   - <a-descriptions-item label="预计时间">{{ queryResult.estimatedMinutes }} 分钟</a-descriptions-item>
41   - </a-descriptions>
  35 + <div v-if="queryResult" class="soft-page-stack">
  36 + <div class="soft-note-card">
  37 + <strong>订单查询结果</strong>
  38 + <p>这里展示开放平台配送单的核心状态和计费结果,适合按外部订单号快速核对。</p>
  39 + </div>
  40 + <a-descriptions :column="1" bordered size="small">
  41 + <a-descriptions-item label="配送订单ID">{{ queryResult.deliveryOrderId }}</a-descriptions-item>
  42 + <a-descriptions-item label="外部订单号">{{ queryResult.outOrderNo }}</a-descriptions-item>
  43 + <a-descriptions-item label="状态">{{ statusMap[queryResult.status] }}</a-descriptions-item>
  44 + <a-descriptions-item label="配送费">¥{{ queryResult.totalFee }}</a-descriptions-item>
  45 + <a-descriptions-item label="距离">{{ queryResult.distance }} km</a-descriptions-item>
  46 + <a-descriptions-item label="预计时间">{{ queryResult.estimatedMinutes }} 分钟</a-descriptions-item>
  47 + </a-descriptions>
  48 + </div>
42 49 </a-modal>
43 50 </div>
44 51 </template>
... ...
src/views/merchant/EnterList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="入驻申请" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-select v-model:value="filterStatus" placeholder="状态" allowClear style="width:120px" @change="loadList">
  3 + <a-card title="入驻申请" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-select v-model:value="filterStatus" placeholder="状态" allowClear class="list-filter" @change="loadList">
7 7 <a-select-option :value="0">未处理</a-select-option>
8 8 <a-select-option :value="1">已通过</a-select-option>
9 9 <a-select-option :value="-1">已拒绝</a-select-option>
10 10 </a-select>
11   - <a-select v-model:value="filterType" placeholder="类型" allowClear style="width:120px" @change="loadList">
  11 + <a-select v-model:value="filterType" placeholder="类型" allowClear class="list-filter" @change="loadList">
12 12 <a-select-option :value="1">商家入驻</a-select-option>
13 13 <a-select-option :value="2">骑手入驻</a-select-option>
14 14 <a-select-option :value="3">商务合作</a-select-option>
15 15 </a-select>
16   - </a-space>
17   - </template>
  16 + </div>
  17 + </div>
18 18 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
19 19 <template #bodyCell="{ column, record }">
20 20 <template v-if="column.key === 'type'">
... ...
src/views/merchant/StoreList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="店铺管理" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-select v-model:value="filterCityId" placeholder="选择租户" allowClear style="width:150px" @change="loadList">
  3 + <a-card title="店铺管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-select v-model:value="filterCityId" placeholder="选择租户" allowClear class="list-filter" @change="loadList">
7 7 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
8 8 </a-select>
9   - <a-input-search v-model:value="keyword" placeholder="搜索店铺名" @search="loadList" style="width:200px" />
  9 + <a-input-search v-model:value="keyword" placeholder="搜索店铺名" @search="loadList" class="list-search" />
  10 + </div>
  11 + <div class="list-toolbar-right">
10 12 <a-button type="primary" @click="openAdd">新增店铺</a-button>
11   - </a-space>
12   - </template>
  13 + </div>
  14 + </div>
13 15 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
14 16 <template #bodyCell="{ column, record }">
15 17 <template v-if="column.key === 'operatingState'">
... ... @@ -17,6 +19,9 @@
17 19 {{ record.operatingState === 1 ? '营业中' : '打烊' }}
18 20 </a-tag>
19 21 </template>
  22 + <template v-if="column.key === 'cityId'">
  23 + {{ getCityName(record.cityId) }}
  24 + </template>
20 25 <template v-if="column.key === 'shippingType'">
21 26 {{ record.shippingType === 1 ? '外卖配送' : '到店自提' }}
22 27 </template>
... ... @@ -38,54 +43,63 @@
38 43 </a-table>
39 44 </a-card>
40 45  
41   - <!-- 新增/编辑弹窗 -->
42   - <a-modal v-model:open="modalVisible" :title="editingId ? '编辑店铺' : '新增店铺'"
43   - @ok="handleSave" :confirmLoading="saving" width="600px">
44   - <a-form :model="form" layout="vertical">
45   - <a-form-item label="店铺名称"><a-input v-model:value="form.name" /></a-form-item>
46   - <a-form-item label="所属租户">
47   - <a-select v-model:value="form.cityId" placeholder="选择租户">
48   - <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
49   - </a-select>
50   - </a-form-item>
51   - <a-form-item label="外部门店编号(选填,接入方对账用)">
52   - <a-input v-model:value="form.outStoreId" placeholder="接入方自己系统的门店ID" />
53   - </a-form-item>
54   - <a-form-item label="店铺地址"><a-input v-model:value="form.address" /></a-form-item>
55   - <a-row :gutter="16">
56   - <a-col :span="12">
57   - <a-form-item label="经度"><a-input v-model:value="form.lng" /></a-form-item>
58   - </a-col>
59   - <a-col :span="12">
60   - <a-form-item label="纬度"><a-input v-model:value="form.lat" /></a-form-item>
61   - </a-col>
62   - </a-row>
63   - <a-form-item label="配送类型">
64   - <a-radio-group v-model:value="form.shippingType">
65   - <a-radio :value="1">外卖配送</a-radio>
66   - <a-radio :value="2">到店自提</a-radio>
67   - </a-radio-group>
68   - </a-form-item>
69   - <a-form-item label="自动接单">
70   - <a-switch v-model:checked="form.automaticOrder" :checked-value="1" :un-checked-value="0" />
71   - </a-form-item>
72   - <a-form-item label="商家账号手机号(新增时创建登录账号)" v-if="!editingId">
73   - <a-input v-model:value="form.accountMobile" placeholder="选填" />
74   - </a-form-item>
75   - <a-form-item label="店铺简介"><a-textarea v-model:value="form.about" :rows="3" /></a-form-item>
76   - </a-form>
  46 + <a-modal v-model:open="modalVisible" :title="editingId ? '编辑店铺' : '新增店铺'" @ok="handleSave" :confirmLoading="saving" width="600px">
  47 + <div class="soft-page-stack">
  48 + <div class="soft-note-card">
  49 + <strong>店铺资料说明</strong>
  50 + <p>当前页面只维护外卖相关店铺信息。新增时可选填商家账号手机号,系统会同步创建登录账号。</p>
  51 + </div>
  52 + <a-form :model="form" layout="vertical">
  53 + <a-form-item label="店铺名称"><a-input v-model:value="form.name" /></a-form-item>
  54 + <a-form-item label="所属租户">
  55 + <a-select v-model:value="form.cityId" placeholder="选择租户">
  56 + <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
  57 + </a-select>
  58 + </a-form-item>
  59 + <a-form-item label="外部门店编号(选填,接入方对账用)">
  60 + <a-input v-model:value="form.outStoreId" placeholder="接入方自己系统的门店ID" />
  61 + </a-form-item>
  62 + <a-form-item label="店铺地址"><a-input v-model:value="form.address" /></a-form-item>
  63 + <a-row :gutter="16">
  64 + <a-col :span="12">
  65 + <a-form-item label="经度"><a-input v-model:value="form.lng" /></a-form-item>
  66 + </a-col>
  67 + <a-col :span="12">
  68 + <a-form-item label="纬度"><a-input v-model:value="form.lat" /></a-form-item>
  69 + </a-col>
  70 + </a-row>
  71 + <a-form-item label="配送类型">
  72 + <a-radio-group v-model:value="form.shippingType">
  73 + <a-radio :value="1">外卖配送</a-radio>
  74 + <a-radio :value="2">到店自提</a-radio>
  75 + </a-radio-group>
  76 + </a-form-item>
  77 + <a-form-item label="自动接单">
  78 + <a-switch v-model:checked="form.automaticOrder" :checked-value="1" :un-checked-value="0" />
  79 + </a-form-item>
  80 + <a-form-item label="商家账号手机号(新增时创建登录账号)" v-if="!editingId">
  81 + <a-input v-model:value="form.accountMobile" placeholder="选填" />
  82 + </a-form-item>
  83 + <a-form-item label="店铺简介"><a-textarea v-model:value="form.about" :rows="3" /></a-form-item>
  84 + </a-form>
  85 + </div>
77 86 </a-modal>
78 87  
79   - <!-- 费用配置弹窗 -->
80 88 <a-modal v-model:open="feeVisible" title="费用配置" @ok="handleFeeSave" :confirmLoading="saving">
81   - <a-form layout="vertical">
82   - <a-form-item label="免运费门槛(元,0=不免运费)">
83   - <a-input-number v-model:value="feeForm.freeShipping" :min="0" style="width:100%" />
84   - </a-form-item>
85   - <a-form-item label="起送金额(元,0=不限)">
86   - <a-input-number v-model:value="feeForm.upToSend" :min="0" style="width:100%" />
87   - </a-form-item>
88   - </a-form>
  89 + <div class="soft-page-stack">
  90 + <div class="soft-note-card">
  91 + <strong>店铺费用配置</strong>
  92 + <p>这里只配置店铺侧起送和免运费门槛,不替代租户计价方案;最终配送费仍按租户默认计价方案计算。</p>
  93 + </div>
  94 + <a-form layout="vertical">
  95 + <a-form-item label="免运费门槛(元,0=不免运费)">
  96 + <a-input-number v-model:value="feeForm.freeShipping" :min="0" style="width:100%" />
  97 + </a-form-item>
  98 + <a-form-item label="起送金额(元,0=不限)">
  99 + <a-input-number v-model:value="feeForm.upToSend" :min="0" style="width:100%" />
  100 + </a-form-item>
  101 + </a-form>
  102 + </div>
89 103 </a-modal>
90 104 </div>
91 105 </template>
... ... @@ -111,7 +125,7 @@ const feeForm = reactive({ freeShipping: 0, upToSend: 0 })
111 125 const columns = [
112 126 { title: 'ID', dataIndex: 'id', width: 80 },
113 127 { title: '店铺名', dataIndex: 'name' },
114   - { title: '租户', dataIndex: 'cityId' },
  128 + { title: '租户', key: 'cityId' },
115 129 { title: '外部编号', dataIndex: 'outStoreId', ellipsis: true },
116 130 { title: '接入方', dataIndex: 'appKey', ellipsis: true },
117 131 { title: '地址', dataIndex: 'address', ellipsis: true },
... ... @@ -133,6 +147,11 @@ async function loadCities() {
133 147 cityList.value = res.data
134 148 }
135 149  
  150 +function getCityName(cityId?: number) {
  151 + const city = cityList.value.find(item => item.id === cityId)
  152 + return city?.name || (cityId ? `租户#${cityId}` : '-')
  153 +}
  154 +
136 155 function openAdd() {
137 156 editingId.value = null
138 157 Object.assign(form, { name: '', cityId: undefined, address: '', lng: '', lat: '', shippingType: 1, automaticOrder: 0, accountMobile: '', about: '', outStoreId: '' })
... ...
src/views/open/OpenAppList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="开放平台应用管理" :bordered="false">
4   - <template #extra>
5   - <a-button type="primary" @click="openAdd">创建应用</a-button>
6   - </template>
  3 + <a-card title="开放平台应用管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-right">
  6 + <a-button type="primary" @click="openAdd">创建应用</a-button>
  7 + </div>
  8 + </div>
7 9 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
8 10 <template #bodyCell="{ column, record }">
9 11 <template v-if="column.key === 'status'">
... ... @@ -14,6 +16,9 @@
14 16 <template v-if="column.key === 'appKey'">
15 17 <a-typography-text copyable>{{ record.appKey }}</a-typography-text>
16 18 </template>
  19 + <template v-if="column.key === 'cityId'">
  20 + {{ getCityName(record.cityId) }}
  21 + </template>
17 22 <template v-if="column.key === 'action'">
18 23 <a-space>
19 24 <a @click="handleResetSecret(record)">重置密钥</a>
... ... @@ -32,47 +37,65 @@
32 37 </a-table>
33 38 </a-card>
34 39  
35   - <!-- 创建应用 -->
36 40 <a-modal v-model:open="addVisible" title="创建应用" @ok="handleCreate" :confirmLoading="saving">
37   - <a-form :model="addForm" layout="vertical">
38   - <a-form-item label="应用名称"><a-input v-model:value="addForm.appName" /></a-form-item>
39   - <a-form-item label="关联租户(必填)">
40   - <a-select v-model:value="addForm.cityId" placeholder="选择租户" style="width:100%">
41   - <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
42   - </a-select>
43   - </a-form-item>
44   - <a-form-item label="备注"><a-input v-model:value="addForm.remark" /></a-form-item>
45   - </a-form>
  41 + <div class="soft-page-stack">
  42 + <div class="soft-note-card">
  43 + <strong>应用创建说明</strong>
  44 + <p>开放应用会绑定到单一租户,后续模拟推单和对外计价都会基于该租户的配置执行。</p>
  45 + </div>
  46 + <a-form :model="addForm" layout="vertical">
  47 + <a-form-item label="应用名称"><a-input v-model:value="addForm.appName" /></a-form-item>
  48 + <a-form-item label="关联租户(必填)">
  49 + <a-select v-model:value="addForm.cityId" placeholder="选择租户" style="width:100%">
  50 + <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
  51 + </a-select>
  52 + </a-form-item>
  53 + <a-form-item label="备注"><a-input v-model:value="addForm.remark" /></a-form-item>
  54 + </a-form>
  55 + </div>
46 56 </a-modal>
47 57  
48   - <!-- Webhook配置 -->
49 58 <a-modal v-model:open="webhookVisible" title="Webhook配置" @ok="handleWebhookSave" :confirmLoading="saving">
50   - <a-form layout="vertical">
51   - <a-form-item label="回调地址">
52   - <a-input v-model:value="webhookForm.webhookUrl" placeholder="https://your-server.com/webhook" />
53   - </a-form-item>
54   - <a-form-item label="订阅事件(JSON数组)">
55   - <a-textarea v-model:value="webhookForm.webhookEvents"
56   - placeholder='["order.paid","order.completed","order.cancelled"]' :rows="4" />
57   - </a-form-item>
58   - <a-alert message="支持事件:order.paid / order.accepted / order.completed / order.cancelled / order.refund" type="info" show-icon />
59   - </a-form>
  59 + <div class="soft-page-stack">
  60 + <div class="soft-note-card">
  61 + <strong>Webhook说明</strong>
  62 + <p>推送日志页可查看回调结果,建议只订阅当前业务实际需要的事件,避免无效重试。</p>
  63 + </div>
  64 + <a-form layout="vertical">
  65 + <a-form-item label="回调地址">
  66 + <a-input v-model:value="webhookForm.webhookUrl" placeholder="https://your-server.com/webhook" />
  67 + </a-form-item>
  68 + <a-form-item label="订阅事件(JSON数组)">
  69 + <a-textarea
  70 + v-model:value="webhookForm.webhookEvents"
  71 + placeholder='["order.paid","order.completed","order.cancelled"]'
  72 + :rows="4"
  73 + />
  74 + </a-form-item>
  75 + <a-alert message="支持事件:order.paid / order.accepted / order.completed / order.cancelled / order.refund" type="info" show-icon />
  76 + </a-form>
  77 + </div>
60 78 </a-modal>
61 79  
62   - <!-- 推送日志 -->
63 80 <a-modal v-model:open="logsVisible" title="Webhook推送日志" :footer="null" width="800px">
64   - <a-table :dataSource="logs" :columns="logColumns" rowKey="id" size="small" :pagination="false">
65   - <template #bodyCell="{ column, record }">
66   - <template v-if="column.key === 'status'">
67   - <a-tag :color="record.status === 1 ? 'green' : 'red'">
68   - {{ record.status === 1 ? '成功' : '失败' }}
69   - </a-tag>
  81 + <div class="soft-page-stack">
  82 + <div class="soft-note-card">
  83 + <strong>推送日志</strong>
  84 + <p>失败记录支持手动重试,便于排查接入方回调地址或签名处理异常。</p>
  85 + </div>
  86 + <a-table :dataSource="logs" :columns="logColumns" rowKey="id" size="small" :pagination="false">
  87 + <template #bodyCell="{ column, record }">
  88 + <template v-if="column.key === 'status'">
  89 + <a-tag :color="record.status === 1 ? 'green' : 'red'">
  90 + {{ record.status === 1 ? '成功' : '失败' }}
  91 + </a-tag>
  92 + </template>
  93 + <template v-if="column.key === 'action'">
  94 + <a v-if="record.status === 0" @click="retryLog(record.id)">重试</a>
  95 + </template>
70 96 </template>
71   - <template v-if="column.key === 'action'">
72   - <a v-if="record.status === 0" @click="retryLog(record.id)">重试</a>
73   - </template>
74   - </template>
75   - </a-table>
  97 + </a-table>
  98 + </div>
76 99 </a-modal>
77 100 </div>
78 101 </template>
... ... @@ -98,7 +121,7 @@ const columns = [
98 121 { title: 'ID', dataIndex: 'id', width: 80 },
99 122 { title: '应用名称', dataIndex: 'appName' },
100 123 { title: 'AppKey', key: 'appKey' },
101   - { title: '租户', dataIndex: 'cityId' },
  124 + { title: '租户', key: 'cityId' },
102 125 { title: '状态', key: 'status' },
103 126 { title: '回调地址', dataIndex: 'webhookUrl', ellipsis: true },
104 127 { title: '操作', key: 'action' },
... ... @@ -127,6 +150,11 @@ async function loadCities() {
127 150 cityList.value = res.data
128 151 }
129 152  
  153 +function getCityName(cityId?: number) {
  154 + const city = cityList.value.find(item => item.id === cityId)
  155 + return city?.name || (cityId ? `租户#${cityId}` : '-')
  156 +}
  157 +
130 158 function openAdd() {
131 159 Object.assign(addForm, { appName: '', cityId: undefined, remark: '' })
132 160 addVisible.value = true
... ...
src/views/open/OpenMockDelivery.vue
1 1 <template>
2   - <div class="mock-delivery-page">
3   - <a-card title="模拟推送配送单" :bordered="false">
4   - <template #extra>
5   - <a-space>
  2 + <div class="mock-delivery-page soft-page-stack">
  3 + <a-card :bordered="false" class="soft-section-card">
  4 + <div class="soft-section-header">
  5 + <div class="soft-section-heading">
  6 + <h3 class="soft-section-title">模拟推送配送单</h3>
  7 + <p class="soft-section-subtitle">按开放应用模拟创建外卖配送单,并在发送前直接试算当前租户的配送费。</p>
  8 + </div>
  9 + <div class="soft-section-actions">
6 10 <a-button @click="fillDemo">填充示例</a-button>
7 11 <a-button @click="resetForm">重置</a-button>
8 12 <a-button :loading="calcLoading" @click="handleCalcFee">按应用租户试算配送费</a-button>
9 13 <a-button type="primary" :loading="saving" @click="handleSubmit">发送推单</a-button>
10   - </a-space>
11   - </template>
12   -
  14 + </div>
  15 + </div>
  16 +
  17 + <div class="soft-note-card">
  18 + <strong>使用说明</strong>
  19 + <p>系统会根据当前开放应用自动识别所属租户。推单页只覆盖外卖场景,门店可选填接入方门店编号,未填写时再手动补全门店信息。</p>
  20 + </div>
  21 +
  22 + <div class="mock-form-section">
  23 + <div class="soft-section-header">
  24 + <div class="soft-section-heading">
  25 + <h3 class="soft-section-title">应用与订单基础信息</h3>
  26 + <p class="soft-section-subtitle">先确定开放应用、租户和外部订单标识。</p>
  27 + </div>
  28 + </div>
13 29 <a-form layout="vertical">
14 30 <a-row :gutter="16">
15 31 <a-col :xs="24" :lg="12">
... ... @@ -47,100 +63,121 @@
47 63 </a-col>
48 64 </a-row>
49 65  
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>
  66 + <div class="mock-form-section">
  67 + <div class="soft-section-header">
  68 + <div class="soft-section-heading">
  69 + <h3 class="soft-section-title">发货方信息</h3>
  70 + <p class="soft-section-subtitle">如果已同步门店,优先填写接入方门店编号;否则补全门店名称、地址和经纬度。</p>
  71 + </div>
  72 + </div>
  73 + <a-row :gutter="16">
  74 + <a-col :xs="24" :lg="12">
  75 + <a-form-item label="接入方门店编号">
  76 + <a-input v-model:value="form.outStoreId" placeholder="可选,填写后可自动补门店信息" />
  77 + </a-form-item>
  78 + </a-col>
  79 + <a-col :xs="24" :lg="12">
  80 + <a-form-item label="发货门店名称">
  81 + <a-input v-model:value="form.storeName" placeholder="未填写门店编号时必填" />
  82 + </a-form-item>
  83 + </a-col>
  84 + </a-row>
  85 + <a-form-item label="发货门店地址">
  86 + <a-input v-model:value="form.storeAddr" placeholder="请输入发货门店地址" />
  87 + </a-form-item>
  88 + <a-row :gutter="16">
  89 + <a-col :xs="24" :lg="12">
  90 + <a-form-item label="发货经度">
  91 + <a-input v-model:value="form.storeLng" placeholder="如:113.264385" />
  92 + </a-form-item>
  93 + </a-col>
  94 + <a-col :xs="24" :lg="12">
  95 + <a-form-item label="发货纬度">
  96 + <a-input v-model:value="form.storeLat" placeholder="如:23.129112" />
  97 + </a-form-item>
  98 + </a-col>
  99 + </a-row>
  100 + </div>
84 101  
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>
  102 + <div class="mock-form-section">
  103 + <div class="soft-section-header">
  104 + <div class="soft-section-heading">
  105 + <h3 class="soft-section-title">收件人信息</h3>
  106 + <p class="soft-section-subtitle">用于试算距离和创建配送单,收件人地址与经纬度建议同时填写。</p>
  107 + </div>
  108 + </div>
  109 + <a-row :gutter="16">
  110 + <a-col :xs="24" :lg="12">
  111 + <a-form-item label="收件人姓名">
  112 + <a-input v-model:value="form.recipName" placeholder="请输入收件人姓名" />
  113 + </a-form-item>
  114 + </a-col>
  115 + <a-col :xs="24" :lg="12">
  116 + <a-form-item label="收件人电话">
  117 + <a-input v-model:value="form.recipPhone" placeholder="请输入收件人手机号" />
  118 + </a-form-item>
  119 + </a-col>
  120 + </a-row>
  121 + <a-form-item label="收件人地址">
  122 + <a-input v-model:value="form.recipAddr" placeholder="请输入收件地址" />
  123 + </a-form-item>
  124 + <a-row :gutter="16">
  125 + <a-col :xs="24" :lg="12">
  126 + <a-form-item label="收件经度">
  127 + <a-input v-model:value="form.recipLng" placeholder="如:113.270000" />
  128 + </a-form-item>
  129 + </a-col>
  130 + <a-col :xs="24" :lg="12">
  131 + <a-form-item label="收件纬度">
  132 + <a-input v-model:value="form.recipLat" placeholder="如:23.135000" />
  133 + </a-form-item>
  134 + </a-col>
  135 + </a-row>
  136 + </div>
113 137  
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>
  138 + <div class="mock-form-section">
  139 + <div class="soft-section-header">
  140 + <div class="soft-section-heading">
  141 + <h3 class="soft-section-title">订单信息</h3>
  142 + <p class="soft-section-subtitle">当前只处理外卖场景,支持重量、整单备注和应用级回调。</p>
  143 + </div>
  144 + </div>
  145 + <a-row :gutter="16">
  146 + <a-col :xs="24" :lg="12">
  147 + <a-form-item label="货物重量(kg)">
  148 + <a-input-number v-model:value="form.weight" :min="0" :step="0.1" style="width:100%" />
  149 + </a-form-item>
  150 + </a-col>
  151 + <a-col :xs="24" :lg="12">
  152 + <a-form-item label="订单级回调地址">
  153 + <a-input v-model:value="form.callbackUrl" placeholder="可选,留空则使用应用级 Webhook" />
  154 + </a-form-item>
  155 + </a-col>
  156 + </a-row>
  157 + <a-form-item label="整单备注">
  158 + <a-textarea v-model:value="form.remark" :rows="3" placeholder="如:请尽快送达" />
  159 + </a-form-item>
  160 + <a-form-item label="货物备注">
  161 + <a-textarea v-model:value="form.itemRemark" :rows="2" placeholder="如:饮品分开装、轻拿轻放" />
  162 + </a-form-item>
137 163 </div>
138   - <div v-if="form.items.length">
  164 +
  165 + <div class="mock-form-section">
  166 + <div class="soft-section-header">
  167 + <div class="soft-section-heading">
  168 + <h3 class="soft-section-title">货物清单</h3>
  169 + <p class="soft-section-subtitle">件数会参与配送费试算,名称为空的货物不会提交到后端。</p>
  170 + </div>
  171 + <div class="soft-section-actions">
  172 + <a-button type="dashed" @click="addItem">新增货物</a-button>
  173 + </div>
  174 + </div>
  175 + <div v-if="form.items.length" class="soft-dashed-block">
139 176 <a-row
140 177 v-for="(item, index) in form.items"
141 178 :key="index"
142 179 :gutter="12"
143   - style="margin-bottom:12px;align-items:flex-start"
  180 + class="mock-item-row"
144 181 >
145 182 <a-col :xs="24" :lg="6">
146 183 <a-form-item :label="index === 0 ? '名称' : ''">
... ... @@ -168,18 +205,21 @@
168 205 </a-form-item>
169 206 </a-col>
170 207 </a-row>
  208 + </div>
  209 + <a-empty v-else description="暂无货物清单" />
171 210 </div>
172   - <a-empty v-else description="暂无货物清单" />
173 211 </a-form>
  212 + </div>
174 213 </a-card>
175 214  
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   - />
  215 + <div v-if="feeResult || result" class="soft-result-grid">
  216 + <a-card v-if="feeResult" :bordered="false" class="soft-section-card">
  217 + <div class="soft-section-header">
  218 + <div class="soft-section-heading">
  219 + <h3 class="soft-section-title">按应用租户试算结果</h3>
  220 + <p class="soft-section-subtitle">试算时不会手填租户,系统会自动使用当前开放应用绑定的租户;实际推单金额以后端创建订单时实时计算为准。</p>
  221 + </div>
  222 + </div>
183 223 <a-descriptions bordered :column="2">
184 224 <a-descriptions-item label="配送费">¥{{ feeResult.totalFee }}</a-descriptions-item>
185 225 <a-descriptions-item label="配送距离">{{ feeResult.distance }} km</a-descriptions-item>
... ... @@ -201,9 +241,15 @@
201 241 <a-descriptions-item label="计费重量">{{ feeResult.weight }} kg</a-descriptions-item>
202 242 <a-descriptions-item label="计费件数">{{ feeResult.pieces || 0 }} 件</a-descriptions-item>
203 243 </a-descriptions>
204   - </a-card>
205   -
206   - <a-card v-if="result" title="推单结果" :bordered="false">
  244 + </a-card>
  245 +
  246 + <a-card v-if="result" :bordered="false" class="soft-section-card">
  247 + <div class="soft-section-header">
  248 + <div class="soft-section-heading">
  249 + <h3 class="soft-section-title">推单结果</h3>
  250 + <p class="soft-section-subtitle">展示后台创建后的配送单编号、状态和费用结果。</p>
  251 + </div>
  252 + </div>
207 253 <a-descriptions bordered :column="2">
208 254 <a-descriptions-item label="配送单ID">{{ result.deliveryOrderId }}</a-descriptions-item>
209 255 <a-descriptions-item label="平台订单号">{{ result.orderNo }}</a-descriptions-item>
... ... @@ -216,7 +262,8 @@
216 262 <a-descriptions-item label="时段附加费">¥{{ result.moneyTime }}</a-descriptions-item>
217 263 <a-descriptions-item label="预计送达">{{ result.estimatedMinutes }} 分钟</a-descriptions-item>
218 264 </a-descriptions>
219   - </a-card>
  265 + </a-card>
  266 + </div>
220 267 </div>
221 268 </template>
222 269  
... ... @@ -470,7 +517,17 @@ watch(
470 517  
471 518 <style scoped>
472 519 .mock-delivery-page {
473   - display: grid;
474   - gap: 18px;
  520 + gap: 16px;
  521 +}
  522 +
  523 +.mock-form-section {
  524 + margin-top: 8px;
  525 + padding-top: 18px;
  526 + border-top: 1px solid rgba(206, 196, 244, 0.22);
  527 +}
  528 +
  529 +.mock-item-row {
  530 + margin-bottom: 12px;
  531 + align-items: flex-start;
475 532 }
476 533 </style>
... ...
src/views/order/OrderList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="订单管理" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-select v-model:value="filterStatus" placeholder="订单状态" allowClear style="width:130px" @change="loadList">
  3 + <a-card title="订单管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-select v-model:value="filterStatus" placeholder="订单状态" allowClear class="list-filter" @change="loadList">
7 7 <a-select-option :value="2">已支付</a-select-option>
8 8 <a-select-option :value="3">已接单</a-select-option>
9 9 <a-select-option :value="4">服务中</a-select-option>
... ... @@ -11,14 +11,14 @@
11 11 <a-select-option :value="7">退款申请</a-select-option>
12 12 <a-select-option :value="10">已取消</a-select-option>
13 13 </a-select>
14   - <a-select v-model:value="filterTrans" placeholder="转单状态" allowClear style="width:130px" @change="loadList">
  14 + <a-select v-model:value="filterTrans" placeholder="转单状态" allowClear class="list-filter" @change="loadList">
15 15 <a-select-option :value="2">转单申请中</a-select-option>
16 16 <a-select-option :value="1">已转单</a-select-option>
17 17 <a-select-option :value="3">转单拒绝</a-select-option>
18 18 </a-select>
19   - <a-input-search v-model:value="keyword" placeholder="订单号" @search="loadList" style="width:200px" />
20   - </a-space>
21   - </template>
  19 + <a-input-search v-model:value="keyword" placeholder="订单号" @search="loadList" class="list-search" />
  20 + </div>
  21 + </div>
22 22 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
23 23 <template #bodyCell="{ column, record }">
24 24 <template v-if="column.key === 'status'">
... ... @@ -48,57 +48,72 @@
48 48 </a-table>
49 49 </a-card>
50 50  
51   - <!-- 指派骑手弹窗 -->
52 51 <a-modal v-model:open="designateVisible" title="指派骑手" @ok="handleDesignate" :confirmLoading="saving">
53   - <a-form layout="vertical">
54   - <a-form-item label="选择骑手">
55   - <a-select
56   - v-model:value="designateRiderId"
57   - style="width:100%"
58   - placeholder="请选择可指派骑手"
59   - :loading="candidateLoading"
60   - show-search
61   - :filter-option="filterCandidateOption"
62   - option-label-prop="label"
63   - >
64   - <a-select-option
65   - v-for="item in designateCandidates"
66   - :key="item.id"
67   - :value="item.id"
68   - :label="`${item.userNickname || '未命名'}(ID:${item.id})`"
  52 + <div class="soft-page-stack">
  53 + <div class="soft-note-card">
  54 + <strong>指派说明</strong>
  55 + <p>这里只展示当前订单可指派的骑手候选,列表中会带出手机号和在线/休息状态,避免只能靠 ID 操作。</p>
  56 + </div>
  57 + <a-form layout="vertical">
  58 + <a-form-item label="选择骑手">
  59 + <a-select
  60 + v-model:value="designateRiderId"
  61 + style="width:100%"
  62 + placeholder="请选择可指派骑手"
  63 + :loading="candidateLoading"
  64 + show-search
  65 + :filter-option="filterCandidateOption"
  66 + option-label-prop="label"
69 67 >
70   - {{ item.userNickname || '未命名' }}(ID:{{ item.id }} / {{ item.mobile || '无手机号' }} / {{ item.isRest === 1 ? '休息' : '在线' }})
71   - </a-select-option>
72   - </a-select>
73   - </a-form-item>
74   - </a-form>
  68 + <a-select-option
  69 + v-for="item in designateCandidates"
  70 + :key="item.id"
  71 + :value="item.id"
  72 + :label="`${item.userNickname || '未命名'}(ID:${item.id})`"
  73 + >
  74 + {{ item.userNickname || '未命名' }}(ID:{{ item.id }} / {{ item.mobile || '无手机号' }} / {{ item.isRest === 1 ? '休息' : '在线' }})
  75 + </a-select-option>
  76 + </a-select>
  77 + </a-form-item>
  78 + </a-form>
  79 + </div>
75 80 </a-modal>
76 81  
77   - <!-- 退款记录弹窗 -->
78 82 <a-modal v-model:open="refundVisible" title="退款记录" :footer="null">
79   - <a-descriptions :column="1" bordered size="small" v-if="refundRecord">
80   - <a-descriptions-item label="订单号">{{ refundRecord.orderNo }}</a-descriptions-item>
81   - <a-descriptions-item label="申请角色">{{ refundRecord.role === 1 ? '用户' : '骑手' }}</a-descriptions-item>
82   - <a-descriptions-item label="退款原因">{{ refundRecord.reason }}</a-descriptions-item>
83   - <a-descriptions-item label="退款金额">¥{{ refundRecord.money }}</a-descriptions-item>
84   - <a-descriptions-item label="状态">
85   - <a-tag :color="refundRecord.status === 1 ? 'green' : refundRecord.status === 2 ? 'red' : 'orange'">
86   - {{ ({'0': '待处理', '1': '已通过', '2': '已拒绝'} as Record<string, string>)[String(refundRecord.status)] }}
87   - </a-tag>
88   - </a-descriptions-item>
89   - <a-descriptions-item label="处理备注">{{ refundRecord.remark || '-' }}</a-descriptions-item>
90   - </a-descriptions>
91   - <a-space style="margin-top:16px" v-if="refundRecord && refundRecord.status === 0">
92   - <a-popconfirm title="确认通过退款?" @confirm="handleRefund(1)">
93   - <a-button type="primary">通过退款</a-button>
94   - </a-popconfirm>
95   - <a-button danger @click="openReject">拒绝退款</a-button>
96   - </a-space>
  83 + <div v-if="refundRecord" class="soft-page-stack">
  84 + <div class="soft-note-card">
  85 + <strong>退款处理说明</strong>
  86 + <p>通过或拒绝退款后会更新订单退款状态,处理前建议先核对申请角色、原因和金额。</p>
  87 + </div>
  88 + <a-descriptions :column="1" bordered size="small">
  89 + <a-descriptions-item label="订单号">{{ refundRecord.orderNo }}</a-descriptions-item>
  90 + <a-descriptions-item label="申请角色">{{ refundRecord.role === 1 ? '用户' : '骑手' }}</a-descriptions-item>
  91 + <a-descriptions-item label="退款原因">{{ refundRecord.reason }}</a-descriptions-item>
  92 + <a-descriptions-item label="退款金额">¥{{ refundRecord.money }}</a-descriptions-item>
  93 + <a-descriptions-item label="状态">
  94 + <a-tag :color="refundRecord.status === 1 ? 'green' : refundRecord.status === 2 ? 'red' : 'orange'">
  95 + {{ ({'0': '待处理', '1': '已通过', '2': '已拒绝'} as Record<string, string>)[String(refundRecord.status)] }}
  96 + </a-tag>
  97 + </a-descriptions-item>
  98 + <a-descriptions-item label="处理备注">{{ refundRecord.remark || '-' }}</a-descriptions-item>
  99 + </a-descriptions>
  100 + <div v-if="refundRecord.status === 0" class="soft-inline-actions">
  101 + <a-popconfirm title="确认通过退款?" @confirm="handleRefund(1)">
  102 + <a-button type="primary">通过退款</a-button>
  103 + </a-popconfirm>
  104 + <a-button danger @click="openReject">拒绝退款</a-button>
  105 + </div>
  106 + </div>
97 107 </a-modal>
98 108  
99   - <!-- 拒绝退款弹窗 -->
100 109 <a-modal v-model:open="rejectVisible" title="拒绝退款" @ok="handleRefund(2)" :confirmLoading="saving">
101   - <a-textarea v-model:value="rejectRemark" :rows="3" placeholder="填写拒绝原因" />
  110 + <div class="soft-page-stack">
  111 + <div class="soft-note-card">
  112 + <strong>填写拒绝原因</strong>
  113 + <p>拒绝原因会进入退款处理记录,建议填写明确、可追溯的说明。</p>
  114 + </div>
  115 + <a-textarea v-model:value="rejectRemark" :rows="3" placeholder="填写拒绝原因" />
  116 + </div>
102 117 </a-modal>
103 118 </div>
104 119 </template>
... ...
src/views/order/RefundList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="退款管理" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-select v-model:value="filterStatus" placeholder="状态" allowClear style="width:120px" @change="loadList">
  3 + <a-card title="退款管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-select v-model:value="filterStatus" placeholder="状态" allowClear class="list-filter" @change="loadList">
7 7 <a-select-option :value="0">待处理</a-select-option>
8 8 <a-select-option :value="1">已通过</a-select-option>
9 9 <a-select-option :value="2">已拒绝</a-select-option>
10 10 </a-select>
11   - <a-input-search v-model:value="keyword" placeholder="订单号" @search="loadList" style="width:200px" />
12   - </a-space>
13   - </template>
  11 + <a-input-search v-model:value="keyword" placeholder="订单号" @search="loadList" class="list-search" />
  12 + </div>
  13 + </div>
14 14 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
15 15 <template #bodyCell="{ column, record }">
16 16 <template v-if="column.key === 'role'">
... ...
src/views/rider/RiderEvaluateList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="骑手评价" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-input-number v-model:value="filterRiderId" placeholder="骑手ID" style="width:130px" />
7   - <a-select v-model:value="filterType" placeholder="评价类型" allowClear style="width:120px">
  3 + <a-card title="骑手评价" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-input-number v-model:value="filterRiderId" placeholder="骑手ID" class="list-filter" />
  7 + <a-select v-model:value="filterType" placeholder="评价类型" allowClear class="list-filter">
8 8 <a-select-option :value="0">全部</a-select-option>
9 9 <a-select-option :value="1">好评(4-5星)</a-select-option>
10 10 <a-select-option :value="2">中评(3星)</a-select-option>
11 11 <a-select-option :value="3">差评(1-2星)</a-select-option>
12 12 </a-select>
  13 + </div>
  14 + <div class="list-toolbar-right">
13 15 <a-button type="primary" @click="loadList">查询</a-button>
14   - </a-space>
15   - </template>
  16 + </div>
  17 + </div>
16 18 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
17 19 <template #bodyCell="{ column, record }">
  20 + <template v-if="column.key === 'cityId'">
  21 + {{ getCityName(record.cityId) }}
  22 + </template>
18 23 <template v-if="column.key === 'star'">
19 24 <a-rate :value="record.star" disabled />
20 25 </template>
... ... @@ -25,11 +30,12 @@
25 30 </template>
26 31  
27 32 <script setup lang="ts">
28   -import { ref } from 'vue'
29   -import { riderApi } from '@/api'
  33 +import { ref, onMounted } from 'vue'
  34 +import { cityApi, riderApi } from '@/api'
30 35  
31 36 const loading = ref(false)
32 37 const list = ref<any[]>([])
  38 +const cityList = ref<any[]>([])
33 39 const filterRiderId = ref<number | undefined>()
34 40 const filterType = ref(0)
35 41  
... ... @@ -38,9 +44,19 @@ const columns = [
38 44 { title: '骑手ID', dataIndex: 'rid' },
39 45 { title: '评分', key: 'star' },
40 46 { title: '内容', dataIndex: 'content', ellipsis: true },
41   - { title: '租户', dataIndex: 'cityId' },
  47 + { title: '租户', key: 'cityId' },
42 48 ]
43 49  
  50 +async function loadCities() {
  51 + const res: any = await cityApi.openList()
  52 + cityList.value = Array.isArray(res?.data) ? res.data : []
  53 +}
  54 +
  55 +function getCityName(cityId?: number) {
  56 + const city = cityList.value.find(item => item.id === cityId)
  57 + return city?.name || (cityId ? `租户#${cityId}` : '-')
  58 +}
  59 +
44 60 async function loadList() {
45 61 if (!filterRiderId.value) return
46 62 loading.value = true
... ... @@ -49,4 +65,6 @@ async function loadList() {
49 65 list.value = res.data || []
50 66 } finally { loading.value = false }
51 67 }
  68 +
  69 +onMounted(loadCities)
52 70 </script>
... ...
src/views/rider/RiderList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="骑手管理" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-select v-model:value="filterStatus" placeholder="审核状态" allowClear style="width:120px" @change="loadList">
  3 + <a-card title="骑手管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-select v-model:value="filterStatus" placeholder="审核状态" allowClear class="list-filter" @change="loadList">
7 7 <a-select-option :value="2">待审核</a-select-option>
8 8 <a-select-option :value="1">已通过</a-select-option>
9 9 <a-select-option :value="0">已拒绝</a-select-option>
10 10 </a-select>
11   - <a-input-search v-model:value="keyword" placeholder="搜索姓名/手机" @search="loadList" style="width:200px" />
  11 + <a-input-search v-model:value="keyword" placeholder="搜索姓名/手机" @search="loadList" class="list-search" />
  12 + </div>
  13 + <div class="list-toolbar-right">
12 14 <a-button type="primary" @click="openAdd">新增骑手</a-button>
13   - </a-space>
14   - </template>
  15 + </div>
  16 + </div>
15 17 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
16 18 <template #bodyCell="{ column, record }">
17 19 <template v-if="column.key === 'userStatus'">
... ... @@ -19,6 +21,9 @@
19 21 {{ statusMap[record.userStatus] }}
20 22 </a-tag>
21 23 </template>
  24 + <template v-if="column.key === 'cityId'">
  25 + {{ getCityName(record.cityId) }}
  26 + </template>
22 27 <template v-if="column.key === 'levelName'">
23 28 {{ record.levelName || '默认等级' }}
24 29 </template>
... ... @@ -67,38 +72,50 @@
67 72 </a-card>
68 73  
69 74 <a-modal v-model:open="modalVisible" title="新增骑手" @ok="handleAdd" :confirmLoading="saving">
70   - <a-form :model="form" layout="vertical">
71   - <a-form-item v-if="isAdmin" label="租户">
72   - <a-select v-model:value="form.cityId" placeholder="选择租户">
73   - <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
74   - </a-select>
75   - </a-form-item>
76   - <a-form-item label="昵称">
77   - <a-input v-model:value="form.userNickname" placeholder="请输入骑手昵称" />
78   - </a-form-item>
79   - <a-form-item label="手机号">
80   - <a-input v-model:value="form.mobile" placeholder="请输入手机号" />
81   - </a-form-item>
82   - <a-form-item label="密码">
83   - <a-input-password v-model:value="form.password" placeholder="请输入登录密码" />
84   - </a-form-item>
85   - </a-form>
  75 + <div class="soft-page-stack">
  76 + <div class="soft-note-card">
  77 + <strong>骑手创建说明</strong>
  78 + <p>当前入口用于后台手动新增骑手账号。管理员需要先选择租户,新增后可继续设置等级、启用状态和全职/兼职类型。</p>
  79 + </div>
  80 + <a-form :model="form" layout="vertical">
  81 + <a-form-item v-if="isAdmin" label="租户">
  82 + <a-select v-model:value="form.cityId" placeholder="选择租户">
  83 + <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
  84 + </a-select>
  85 + </a-form-item>
  86 + <a-form-item label="昵称">
  87 + <a-input v-model:value="form.userNickname" placeholder="请输入骑手昵称" />
  88 + </a-form-item>
  89 + <a-form-item label="手机号">
  90 + <a-input v-model:value="form.mobile" placeholder="请输入手机号" />
  91 + </a-form-item>
  92 + <a-form-item label="密码">
  93 + <a-input-password v-model:value="form.password" placeholder="请输入登录密码" />
  94 + </a-form-item>
  95 + </a-form>
  96 + </div>
86 97 </a-modal>
87 98  
88 99 <a-modal v-model:open="levelVisible" title="设置骑手等级" @ok="handleSetLevel" :confirmLoading="levelSaving">
89   - <a-form layout="vertical">
90   - <a-form-item label="骑手">
91   - <a-input :value="levelTargetName" disabled />
92   - </a-form-item>
93   - <a-form-item label="等级">
94   - <a-select v-model:value="selectedLevelId" placeholder="请选择等级">
95   - <a-select-option :value="0">使用默认等级</a-select-option>
96   - <a-select-option v-for="item in levelOptions" :key="item.id" :value="item.id">
97   - {{ item.name }}{{ item.isDefault === 1 ? '(默认)' : '' }}
98   - </a-select-option>
99   - </a-select>
100   - </a-form-item>
101   - </a-form>
  100 + <div class="soft-page-stack">
  101 + <div class="soft-note-card">
  102 + <strong>等级设置说明</strong>
  103 + <p>骑手等级会影响骑手收入规则。未单独指定时,将自动使用当前租户的默认等级。</p>
  104 + </div>
  105 + <a-form layout="vertical">
  106 + <a-form-item label="骑手">
  107 + <a-input :value="levelTargetName" disabled />
  108 + </a-form-item>
  109 + <a-form-item label="等级">
  110 + <a-select v-model:value="selectedLevelId" placeholder="请选择等级">
  111 + <a-select-option :value="0">使用默认等级</a-select-option>
  112 + <a-select-option v-for="item in levelOptions" :key="item.id" :value="item.id">
  113 + {{ item.name }}{{ item.isDefault === 1 ? '(默认)' : '' }}
  114 + </a-select-option>
  115 + </a-select>
  116 + </a-form-item>
  117 + </a-form>
  118 + </div>
102 119 </a-modal>
103 120 </div>
104 121 </template>
... ... @@ -137,7 +154,7 @@ const columns = [
137 154 { title: 'ID', dataIndex: 'id', width: 80 },
138 155 { title: '昵称', dataIndex: 'userNickname' },
139 156 { title: '手机', dataIndex: 'mobile' },
140   - { title: '租户ID', dataIndex: 'cityId' },
  157 + { title: '租户', key: 'cityId' },
141 158 { title: '等级', key: 'levelName' },
142 159 { title: '类型', key: 'type' },
143 160 { title: '审核状态', key: 'userStatus' },
... ... @@ -155,6 +172,11 @@ function getWorkStatus(record: any) {
155 172 return record.isRest === 1 ? 1 : 0
156 173 }
157 174  
  175 +function getCityName(cityId?: number) {
  176 + const city = cityList.value.find(item => item.id === cityId)
  177 + return city?.name || (cityId ? `租户#${cityId}` : '-')
  178 +}
  179 +
158 180 async function loadList() {
159 181 loading.value = true
160 182 try {
... ... @@ -243,8 +265,6 @@ async function setType(riderId: number, type: number) {
243 265  
244 266 onMounted(() => {
245 267 loadList()
246   - if (isAdmin.value) {
247   - loadCities()
248   - }
  268 + loadCities()
249 269 })
250 270 </script>
... ...
src/views/substation/SubstationList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="分站管理" :bordered="false">
4   - <template #extra>
5   - <a-space>
6   - <a-input-search v-model:value="keyword" placeholder="搜索账号/昵称/手机" @search="loadList" style="width:220px" />
  3 + <a-card title="分站管理" :bordered="false" class="list-table-card">
  4 + <div class="list-toolbar">
  5 + <div class="list-toolbar-left">
  6 + <a-input-search v-model:value="keyword" placeholder="搜索账号/昵称/手机" @search="loadList" class="list-search" />
  7 + </div>
  8 + <div class="list-toolbar-right">
7 9 <a-button type="primary" @click="openAdd">新增分站</a-button>
8   - </a-space>
9   - </template>
  10 + </div>
  11 + </div>
10 12 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
11 13 <template #bodyCell="{ column, record }">
  14 + <template v-if="column.key === 'cityId'">
  15 + {{ getCityName(record.cityId) }}
  16 + </template>
12 17 <template v-if="column.key === 'status'">
13 18 <a-tag :color="record.userStatus === 1 ? 'green' : 'red'">
14 19 {{ record.userStatus === 1 ? '正常' : '禁用' }}
... ... @@ -34,39 +39,49 @@
34 39 </a-table>
35 40 </a-card>
36 41  
37   - <a-modal v-model:open="modalVisible" :title="editingId ? '编辑分站' : '新增分站'"
38   - @ok="handleSave" :confirmLoading="saving">
39   - <a-form :model="form" layout="vertical">
40   - <a-form-item label="所属租户">
41   - <a-select v-model:value="form.cityId" placeholder="选择租户">
42   - <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
43   - </a-select>
44   - </a-form-item>
45   - <a-form-item label="登录账号">
46   - <a-input v-model:value="form.userLogin" :disabled="!!editingId" />
47   - </a-form-item>
48   - <a-form-item label="昵称">
49   - <a-input v-model:value="form.userNickname" />
50   - </a-form-item>
51   - <a-form-item label="手机号">
52   - <a-input v-model:value="form.mobile" />
53   - </a-form-item>
54   - <a-form-item :label="editingId ? '新密码(不填不修改)' : '密码'">
55   - <a-input-password v-model:value="form.userPass" />
56   - </a-form-item>
57   - </a-form>
  42 + <a-modal v-model:open="modalVisible" :title="editingId ? '编辑分站' : '新增分站'" @ok="handleSave" :confirmLoading="saving">
  43 + <div class="soft-page-stack">
  44 + <div class="soft-note-card">
  45 + <strong>分站账号说明</strong>
  46 + <p>分站账号绑定单一租户,用于日常站点运营。编辑时登录账号保持不变,密码留空则不修改。</p>
  47 + </div>
  48 + <a-form :model="form" layout="vertical">
  49 + <a-form-item label="所属租户">
  50 + <a-select v-model:value="form.cityId" placeholder="选择租户">
  51 + <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
  52 + </a-select>
  53 + </a-form-item>
  54 + <a-form-item label="登录账号">
  55 + <a-input v-model:value="form.userLogin" :disabled="!!editingId" />
  56 + </a-form-item>
  57 + <a-form-item label="昵称">
  58 + <a-input v-model:value="form.userNickname" />
  59 + </a-form-item>
  60 + <a-form-item label="手机号">
  61 + <a-input v-model:value="form.mobile" />
  62 + </a-form-item>
  63 + <a-form-item :label="editingId ? '新密码(不填不修改)' : '密码'">
  64 + <a-input-password v-model:value="form.userPass" />
  65 + </a-form-item>
  66 + </a-form>
  67 + </div>
58 68 </a-modal>
59 69  
60   - <!-- 改密码弹窗 -->
61 70 <a-modal v-model:open="pwdVisible" title="修改密码" @ok="handleChangePwd" :confirmLoading="pwdSaving">
62   - <a-form layout="vertical">
63   - <a-form-item label="原密码">
64   - <a-input-password v-model:value="pwdForm.oldPassword" />
65   - </a-form-item>
66   - <a-form-item label="新密码">
67   - <a-input-password v-model:value="pwdForm.newPassword" />
68   - </a-form-item>
69   - </a-form>
  71 + <div class="soft-page-stack">
  72 + <div class="soft-note-card">
  73 + <strong>密码修改说明</strong>
  74 + <p>这里修改的是当前选中分站账号的登录密码,提交时会按目标分站账号执行,不影响其他账号。</p>
  75 + </div>
  76 + <a-form layout="vertical">
  77 + <a-form-item label="原密码">
  78 + <a-input-password v-model:value="pwdForm.oldPassword" />
  79 + </a-form-item>
  80 + <a-form-item label="新密码">
  81 + <a-input-password v-model:value="pwdForm.newPassword" />
  82 + </a-form-item>
  83 + </a-form>
  84 + </div>
70 85 </a-modal>
71 86 </div>
72 87 </template>
... ... @@ -90,11 +105,16 @@ const columns = [
90 105 { title: '账号', dataIndex: 'userLogin' },
91 106 { title: '昵称', dataIndex: 'userNickname' },
92 107 { title: '手机', dataIndex: 'mobile' },
93   - { title: '租户ID', dataIndex: 'cityId' },
  108 + { title: '租户', key: 'cityId' },
94 109 { title: '状态', key: 'status' },
95 110 { title: '操作', key: 'action' },
96 111 ]
97 112  
  113 +function getCityName(cityId?: number) {
  114 + const city = cityList.value.find(item => item.id === cityId)
  115 + return city?.name || (cityId ? `租户#${cityId}` : '-')
  116 +}
  117 +
98 118 async function loadList() {
99 119 loading.value = true
100 120 try {
... ... @@ -169,7 +189,7 @@ async function handleChangePwd() {
169 189 }
170 190 pwdSaving.value = true
171 191 try {
172   - await substationApi.changePassword(pwdForm)
  192 + await substationApi.changePassword({ id: pwdTargetId.value, ...pwdForm })
173 193 message.success('密码修改成功')
174 194 pwdVisible.value = false
175 195 } finally { pwdSaving.value = false }
... ...