Commit b37151b626a60e4d06c8403d211396fb3a3aada7

Authored by 杨刚
1 parent 054f8469

init

src/api/index.ts
@@ -35,7 +35,7 @@ export const substationApi = { @@ -35,7 +35,7 @@ export const substationApi = {
35 ban: (id: number) => request.post('/api/platform/substation/ban', null, { params: { id } }), 35 ban: (id: number) => request.post('/api/platform/substation/ban', null, { params: { id } }),
36 cancelBan: (id: number) => request.post('/api/platform/substation/cancelBan', null, { params: { id } }), 36 cancelBan: (id: number) => request.post('/api/platform/substation/cancelBan', null, { params: { id } }),
37 del: (id: number) => request.delete('/api/platform/substation/del', { params: { id } }), 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 request.post('/api/platform/substation/changePassword', data), 39 request.post('/api/platform/substation/changePassword', data),
40 } 40 }
41 41
src/layouts/MainLayout.vue
1 <template> 1 <template>
2 - <div class="layout-shell"> 2 + <div class="layout-shell" :class="{ collapsed }">
3 <aside class="soft-sider" :class="{ collapsed }"> 3 <aside class="soft-sider" :class="{ collapsed }">
4 <button class="sider-toggle" type="button" @click="collapsed = !collapsed"> 4 <button class="sider-toggle" type="button" @click="collapsed = !collapsed">
5 <menu-fold-outlined v-if="!collapsed" /> 5 <menu-fold-outlined v-if="!collapsed" />
@@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
18 <a-menu 18 <a-menu
19 v-model:selectedKeys="selectedKeys" 19 v-model:selectedKeys="selectedKeys"
20 mode="inline" 20 mode="inline"
  21 + :inline-collapsed="collapsed"
21 @click="onMenuClick" 22 @click="onMenuClick"
22 > 23 >
23 <a-menu-item key="/dashboard"> 24 <a-menu-item key="/dashboard">
@@ -163,6 +164,10 @@ function handleLogout() { @@ -163,6 +164,10 @@ function handleLogout() {
163 padding: 16px; 164 padding: 16px;
164 } 165 }
165 166
  167 +.layout-shell.collapsed {
  168 + grid-template-columns: 88px minmax(0, 1fr);
  169 +}
  170 +
166 .soft-sider, 171 .soft-sider,
167 .soft-topbar { 172 .soft-topbar {
168 border: 1px solid rgba(255, 255, 255, 0.58); 173 border: 1px solid rgba(255, 255, 255, 0.58);
@@ -190,6 +195,10 @@ function handleLogout() { @@ -190,6 +195,10 @@ function handleLogout() {
190 padding-right: 4px; 195 padding-right: 4px;
191 } 196 }
192 197
  198 +.soft-sider.collapsed .menu-scroll {
  199 + padding-right: 0;
  200 +}
  201 +
193 .menu-scroll::-webkit-scrollbar { 202 .menu-scroll::-webkit-scrollbar {
194 width: 8px; 203 width: 8px;
195 } 204 }
@@ -201,6 +210,7 @@ function handleLogout() { @@ -201,6 +210,7 @@ function handleLogout() {
201 210
202 .soft-sider.collapsed { 211 .soft-sider.collapsed {
203 width: 88px; 212 width: 88px;
  213 + padding-inline: 8px;
204 } 214 }
205 215
206 .sider-toggle { 216 .sider-toggle {
@@ -215,6 +225,10 @@ function handleLogout() { @@ -215,6 +225,10 @@ function handleLogout() {
215 margin-bottom: 12px; 225 margin-bottom: 12px;
216 } 226 }
217 227
  228 +.soft-sider.collapsed .sider-toggle {
  229 + align-self: center;
  230 +}
  231 +
218 .brand-block { 232 .brand-block {
219 display: flex; 233 display: flex;
220 align-items: center; 234 align-items: center;
@@ -222,6 +236,11 @@ function handleLogout() { @@ -222,6 +236,11 @@ function handleLogout() {
222 padding: 6px 8px 14px; 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 .brand-mark { 244 .brand-mark {
226 width: 44px; 245 width: 44px;
227 height: 44px; 246 height: 44px;
@@ -274,6 +293,10 @@ h1 { @@ -274,6 +293,10 @@ h1 {
274 background: transparent; 293 background: transparent;
275 } 294 }
276 295
  296 +.soft-sider.collapsed :deep(.ant-menu) {
  297 + width: 100%;
  298 +}
  299 +
277 .sider-foot { 300 .sider-foot {
278 margin-top: 10px; 301 margin-top: 10px;
279 flex-shrink: 0; 302 flex-shrink: 0;
@@ -384,6 +407,33 @@ h1 { @@ -384,6 +407,33 @@ h1 {
384 font-size: 13px; 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 @media (max-width: 960px) { 437 @media (max-width: 960px) {
388 .layout-shell { 438 .layout-shell {
389 grid-template-columns: 1fr; 439 grid-template-columns: 1fr;
src/style.css
@@ -226,6 +226,76 @@ a { @@ -226,6 +226,76 @@ a {
226 226
227 .ant-modal .ant-modal-footer { 227 .ant-modal .ant-modal-footer {
228 padding: 12px 20px 18px; 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 .ant-tag { 301 .ant-tag {
@@ -268,6 +338,30 @@ a { @@ -268,6 +338,30 @@ a {
268 color: var(--brand-deep) !important; 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 .ant-menu-item:hover, 365 .ant-menu-item:hover,
272 .ant-menu-submenu-title:hover { 366 .ant-menu-submenu-title:hover {
273 color: var(--brand-deep) !important; 367 color: var(--brand-deep) !important;
@@ -304,8 +398,165 @@ a { @@ -304,8 +398,165 @@ a {
304 pointer-events: none; 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 @media (max-width: 1200px) { 531 @media (max-width: 1200px) {
308 .soft-page-shell { 532 .soft-page-shell {
309 padding: 18px; 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 <template> 1 <template>
2 <div> 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 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 9 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
8 <template #bodyCell="{ column, record }"> 10 <template #bodyCell="{ column, record }">
9 <template v-if="column.key === 'status'"> 11 <template v-if="column.key === 'status'">
@@ -78,6 +80,11 @@ @@ -78,6 +80,11 @@
78 80
79 <div class="plan-content"> 81 <div class="plan-content">
80 <template v-if="currentPlan && config"> 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 <div class="plan-toolbar"> 88 <div class="plan-toolbar">
82 <a-space wrap> 89 <a-space wrap>
83 <a-button @click="copyPlan" :disabled="!selectedPlanId">复制当前方案</a-button> 90 <a-button @click="copyPlan" :disabled="!selectedPlanId">复制当前方案</a-button>
@@ -92,32 +99,42 @@ @@ -92,32 +99,42 @@
92 </a-space> 99 </a-space>
93 </div> 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 <a-card class="preview-card" :bordered="false"> 135 <a-card class="preview-card" :bordered="false">
120 <template #title>草稿试算</template> 136 <template #title>草稿试算</template>
  137 + <div class="soft-section-subtitle preview-subtitle">保存前可先按草稿参数试算配送费,确认距离、时段和附加项是否符合预期。</div>
121 <a-row :gutter="12"> 138 <a-row :gutter="12">
122 <a-col :span="6"> 139 <a-col :span="6">
123 <a-form-item label="起点经度"> 140 <a-form-item label="起点经度">
@@ -172,42 +189,55 @@ @@ -172,42 +189,55 @@
172 </a-card> 189 </a-card>
173 190
174 <a-form :model="config" layout="vertical"> 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 </div> 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 <a-row 236 <a-row
207 v-for="(step, index) in distanceSteps" 237 v-for="(step, index) in distanceSteps"
208 :key="index" 238 :key="index"
209 :gutter="12" 239 :gutter="12"
210 - style="margin-bottom:12px;align-items:flex-start" 240 + class="plan-dynamic-row"
211 > 241 >
212 <a-col :span="7"> 242 <a-col :span="7">
213 <a-form-item :label="index === 0 ? '结束里程(km)' : ''"> 243 <a-form-item :label="index === 0 ? '结束里程(km)' : ''">
@@ -230,45 +260,59 @@ @@ -230,45 +260,59 @@
230 </a-form-item> 260 </a-form-item>
231 </a-col> 261 </a-col>
232 </a-row> 262 </a-row>
  263 + </div>
  264 + <a-empty v-else description="暂无里程阶梯配置" />
233 </div> 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 </div> 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 <a-row 311 <a-row
268 v-for="(rule, index) in pieceRules" 312 v-for="(rule, index) in pieceRules"
269 :key="index" 313 :key="index"
270 :gutter="12" 314 :gutter="12"
271 - style="margin-bottom:12px;align-items:flex-start" 315 + class="plan-dynamic-row"
272 > 316 >
273 <a-col :span="6"> 317 <a-col :span="6">
274 <a-form-item :label="index === 0 ? '起始件数' : ''"> 318 <a-form-item :label="index === 0 ? '起始件数' : ''">
@@ -291,19 +335,26 @@ @@ -291,19 +335,26 @@
291 </a-form-item> 335 </a-form-item>
292 </a-col> 336 </a-col>
293 </a-row> 337 </a-row>
  338 + </div>
  339 + <a-empty v-else description="暂无件数区间配置" />
294 </div> 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 <a-row 353 <a-row
303 v-for="(period, index) in timePeriods" 354 v-for="(period, index) in timePeriods"
304 :key="index" 355 :key="index"
305 :gutter="12" 356 :gutter="12"
306 - style="margin-bottom:12px;align-items:flex-start" 357 + class="plan-dynamic-row"
307 > 358 >
308 <a-col :span="5"> 359 <a-col :span="5">
309 <a-form-item :label="index === 0 ? '开始时间' : ''"> 360 <a-form-item :label="index === 0 ? '开始时间' : ''">
@@ -334,25 +385,33 @@ @@ -334,25 +385,33 @@
334 </a-form-item> 385 </a-form-item>
335 </a-col> 386 </a-col>
336 </a-row> 387 </a-row>
  388 + </div>
  389 + <a-empty v-else description="暂无时段附加费配置" />
337 </div> 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 </a-form> 415 </a-form>
357 </template> 416 </template>
358 <div v-else class="plan-empty-state"> 417 <div v-else class="plan-empty-state">
@@ -1132,6 +1191,10 @@ onMounted(loadList) @@ -1132,6 +1191,10 @@ onMounted(loadList)
1132 padding-right: 4px; 1191 padding-right: 4px;
1133 } 1192 }
1134 1193
  1194 +.plan-note-card {
  1195 + margin-bottom: 16px;
  1196 +}
  1197 +
1135 .plan-toolbar { 1198 .plan-toolbar {
1136 display: flex; 1199 display: flex;
1137 align-items: center; 1200 align-items: center;
@@ -1140,12 +1203,33 @@ onMounted(loadList) @@ -1140,12 +1203,33 @@ onMounted(loadList)
1140 margin-bottom: 16px; 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 .preview-card { 1223 .preview-card {
1144 margin-bottom: 20px; 1224 margin-bottom: 20px;
1145 border-radius: 16px; 1225 border-radius: 16px;
1146 background: #fafbff; 1226 background: #fafbff;
1147 } 1227 }
1148 1228
  1229 +.preview-subtitle {
  1230 + margin-bottom: 14px;
  1231 +}
  1232 +
1149 .preview-result { 1233 .preview-result {
1150 display: flex; 1234 display: flex;
1151 flex-wrap: wrap; 1235 flex-wrap: wrap;
src/views/dashboard/DashboardHome.vue
1 <template> 1 <template>
2 - <div class="dashboard-home"> 2 + <div class="dashboard-home soft-page-stack">
3 <section class="hero-card"> 3 <section class="hero-card">
4 <div class="hero-copy"> 4 <div class="hero-copy">
5 - <div class="hero-pill">Soft-Neo Dashboard</div> 5 + <div class="hero-pill">运营总览</div>
6 <h2>欢迎回到地利外卖运营工作台</h2> 6 <h2>欢迎回到地利外卖运营工作台</h2>
7 - <p>把租户、骑手、订单和分站管理集中在一张更轻盈的首页里,业务页面本身只保留导航与内容,让操作区域更大、更专注。</p> 7 + <p>首页保留总览和快捷入口,业务菜单页只保留导航与内容区域,减少顶部公共模块对操作空间的挤压。</p>
8 <div class="hero-grid"> 8 <div class="hero-grid">
9 <div class="hero-metric"> 9 <div class="hero-metric">
10 - <span>Operation Focus</span>  
11 - <strong>租户 · 骑手 · 订单</strong> 10 + <span>核心范围</span>
  11 + <strong>租户 / 骑手 / 订单</strong>
12 </div> 12 </div>
13 <div class="hero-metric"> 13 <div class="hero-metric">
14 - <span>Visual Mood</span>  
15 - <strong>Lavender / Warm Glow</strong> 14 + <span>当前主题</span>
  15 + <strong>轻柔紫调 / 紧凑运营风格</strong>
16 </div> 16 </div>
17 </div> 17 </div>
18 </div> 18 </div>
@@ -24,7 +24,13 @@ @@ -24,7 +24,13 @@
24 </section> 24 </section>
25 25
26 <section class="content-grid"> 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 <div class="quick-links"> 34 <div class="quick-links">
29 <button v-for="item in quickLinks" :key="item.path" class="quick-link" type="button" @click="go(item.path)"> 35 <button v-for="item in quickLinks" :key="item.path" class="quick-link" type="button" @click="go(item.path)">
30 <strong>{{ item.title }}</strong> 36 <strong>{{ item.title }}</strong>
@@ -33,7 +39,13 @@ @@ -33,7 +39,13 @@
33 </div> 39 </div>
34 </a-card> 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 <ul class="soft-notes"> 49 <ul class="soft-notes">
38 <li>首页保留大视觉和信息卡片,适合做总览和快捷入口。</li> 50 <li>首页保留大视觉和信息卡片,适合做总览和快捷入口。</li>
39 <li>其他菜单页只保留紧凑头部和内容区,避免公共模块挤压表格空间。</li> 51 <li>其他菜单页只保留紧凑头部和内容区,避免公共模块挤压表格空间。</li>
@@ -95,12 +107,18 @@ function go(path: string) { @@ -95,12 +107,18 @@ function go(path: string) {
95 107
96 .hero-copy h2 { 108 .hero-copy h2 {
97 margin: 12px 0 8px; 109 margin: 12px 0 8px;
98 - font-size: 28px; 110 + font-size: 24px;
99 line-height: 1.15; 111 line-height: 1.15;
100 font-family: 'Outfit', sans-serif; 112 font-family: 'Outfit', sans-serif;
101 color: #2f2946; 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 .hero-copy p, 122 .hero-copy p,
105 .hero-metric span, 123 .hero-metric span,
106 .quick-link span, 124 .quick-link span,
@@ -119,7 +137,7 @@ function go(path: string) { @@ -119,7 +137,7 @@ function go(path: string) {
119 min-width: 190px; 137 min-width: 190px;
120 border-radius: 18px; 138 border-radius: 18px;
121 background: rgba(255, 255, 255, 0.74); 139 background: rgba(255, 255, 255, 0.74);
122 - padding: 12px 14px; 140 + padding: 11px 13px;
123 } 141 }
124 142
125 .hero-metric span { 143 .hero-metric span {
@@ -193,9 +211,22 @@ function go(path: string) { @@ -193,9 +211,22 @@ function go(path: string) {
193 cursor: pointer; 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 .soft-notes { 225 .soft-notes {
197 margin: 0; 226 margin: 0;
198 padding-left: 18px; 227 padding-left: 18px;
  228 + font-size: 13px;
  229 + line-height: 1.7;
199 } 230 }
200 231
201 .soft-notes li + li { 232 .soft-notes li + li {
src/views/delivery/DeliveryOrderList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option :value="2">待接单</a-select-option> 7 <a-select-option :value="2">待接单</a-select-option>
8 <a-select-option :value="3">已接单</a-select-option> 8 <a-select-option :value="3">已接单</a-select-option>
9 <a-select-option :value="4">配送中</a-select-option> 9 <a-select-option :value="4">配送中</a-select-option>
10 <a-select-option :value="6">已完成</a-select-option> 10 <a-select-option :value="6">已完成</a-select-option>
11 <a-select-option :value="10">已取消</a-select-option> 11 <a-select-option :value="10">已取消</a-select-option>
12 </a-select> 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 <a-button type="primary" @click="queryByNo" :loading="querying">查询</a-button> 16 <a-button type="primary" @click="queryByNo" :loading="querying">查询</a-button>
15 - </a-space>  
16 - </template> 17 + </div>
  18 + </div>
17 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 19 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
18 <template #bodyCell="{ column, record }"> 20 <template #bodyCell="{ column, record }">
19 <template v-if="column.key === 'status'"> 21 <template v-if="column.key === 'status'">
@@ -29,16 +31,21 @@ @@ -29,16 +31,21 @@
29 </a-table> 31 </a-table>
30 </a-card> 32 </a-card>
31 33
32 - <!-- 查询结果弹窗 -->  
33 <a-modal v-model:open="queryVisible" title="配送订单详情" :footer="null"> 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 </a-modal> 49 </a-modal>
43 </div> 50 </div>
44 </template> 51 </template>
src/views/merchant/EnterList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option :value="0">未处理</a-select-option> 7 <a-select-option :value="0">未处理</a-select-option>
8 <a-select-option :value="1">已通过</a-select-option> 8 <a-select-option :value="1">已通过</a-select-option>
9 <a-select-option :value="-1">已拒绝</a-select-option> 9 <a-select-option :value="-1">已拒绝</a-select-option>
10 </a-select> 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 <a-select-option :value="1">商家入驻</a-select-option> 12 <a-select-option :value="1">商家入驻</a-select-option>
13 <a-select-option :value="2">骑手入驻</a-select-option> 13 <a-select-option :value="2">骑手入驻</a-select-option>
14 <a-select-option :value="3">商务合作</a-select-option> 14 <a-select-option :value="3">商务合作</a-select-option>
15 </a-select> 15 </a-select>
16 - </a-space>  
17 - </template> 16 + </div>
  17 + </div>
18 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 18 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
19 <template #bodyCell="{ column, record }"> 19 <template #bodyCell="{ column, record }">
20 <template v-if="column.key === 'type'"> 20 <template v-if="column.key === 'type'">
src/views/merchant/StoreList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option> 7 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
8 </a-select> 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 <a-button type="primary" @click="openAdd">新增店铺</a-button> 12 <a-button type="primary" @click="openAdd">新增店铺</a-button>
11 - </a-space>  
12 - </template> 13 + </div>
  14 + </div>
13 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 15 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
14 <template #bodyCell="{ column, record }"> 16 <template #bodyCell="{ column, record }">
15 <template v-if="column.key === 'operatingState'"> 17 <template v-if="column.key === 'operatingState'">
@@ -17,6 +19,9 @@ @@ -17,6 +19,9 @@
17 {{ record.operatingState === 1 ? '营业中' : '打烊' }} 19 {{ record.operatingState === 1 ? '营业中' : '打烊' }}
18 </a-tag> 20 </a-tag>
19 </template> 21 </template>
  22 + <template v-if="column.key === 'cityId'">
  23 + {{ getCityName(record.cityId) }}
  24 + </template>
20 <template v-if="column.key === 'shippingType'"> 25 <template v-if="column.key === 'shippingType'">
21 {{ record.shippingType === 1 ? '外卖配送' : '到店自提' }} 26 {{ record.shippingType === 1 ? '外卖配送' : '到店自提' }}
22 </template> 27 </template>
@@ -38,54 +43,63 @@ @@ -38,54 +43,63 @@
38 </a-table> 43 </a-table>
39 </a-card> 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 </a-modal> 86 </a-modal>
78 87
79 - <!-- 费用配置弹窗 -->  
80 <a-modal v-model:open="feeVisible" title="费用配置" @ok="handleFeeSave" :confirmLoading="saving"> 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 </a-modal> 103 </a-modal>
90 </div> 104 </div>
91 </template> 105 </template>
@@ -111,7 +125,7 @@ const feeForm = reactive({ freeShipping: 0, upToSend: 0 }) @@ -111,7 +125,7 @@ const feeForm = reactive({ freeShipping: 0, upToSend: 0 })
111 const columns = [ 125 const columns = [
112 { title: 'ID', dataIndex: 'id', width: 80 }, 126 { title: 'ID', dataIndex: 'id', width: 80 },
113 { title: '店铺名', dataIndex: 'name' }, 127 { title: '店铺名', dataIndex: 'name' },
114 - { title: '租户', dataIndex: 'cityId' }, 128 + { title: '租户', key: 'cityId' },
115 { title: '外部编号', dataIndex: 'outStoreId', ellipsis: true }, 129 { title: '外部编号', dataIndex: 'outStoreId', ellipsis: true },
116 { title: '接入方', dataIndex: 'appKey', ellipsis: true }, 130 { title: '接入方', dataIndex: 'appKey', ellipsis: true },
117 { title: '地址', dataIndex: 'address', ellipsis: true }, 131 { title: '地址', dataIndex: 'address', ellipsis: true },
@@ -133,6 +147,11 @@ async function loadCities() { @@ -133,6 +147,11 @@ async function loadCities() {
133 cityList.value = res.data 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 function openAdd() { 155 function openAdd() {
137 editingId.value = null 156 editingId.value = null
138 Object.assign(form, { name: '', cityId: undefined, address: '', lng: '', lat: '', shippingType: 1, automaticOrder: 0, accountMobile: '', about: '', outStoreId: '' }) 157 Object.assign(form, { name: '', cityId: undefined, address: '', lng: '', lat: '', shippingType: 1, automaticOrder: 0, accountMobile: '', about: '', outStoreId: '' })
src/views/open/OpenAppList.vue
1 <template> 1 <template>
2 <div> 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 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 9 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
8 <template #bodyCell="{ column, record }"> 10 <template #bodyCell="{ column, record }">
9 <template v-if="column.key === 'status'"> 11 <template v-if="column.key === 'status'">
@@ -14,6 +16,9 @@ @@ -14,6 +16,9 @@
14 <template v-if="column.key === 'appKey'"> 16 <template v-if="column.key === 'appKey'">
15 <a-typography-text copyable>{{ record.appKey }}</a-typography-text> 17 <a-typography-text copyable>{{ record.appKey }}</a-typography-text>
16 </template> 18 </template>
  19 + <template v-if="column.key === 'cityId'">
  20 + {{ getCityName(record.cityId) }}
  21 + </template>
17 <template v-if="column.key === 'action'"> 22 <template v-if="column.key === 'action'">
18 <a-space> 23 <a-space>
19 <a @click="handleResetSecret(record)">重置密钥</a> 24 <a @click="handleResetSecret(record)">重置密钥</a>
@@ -32,47 +37,65 @@ @@ -32,47 +37,65 @@
32 </a-table> 37 </a-table>
33 </a-card> 38 </a-card>
34 39
35 - <!-- 创建应用 -->  
36 <a-modal v-model:open="addVisible" title="创建应用" @ok="handleCreate" :confirmLoading="saving"> 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 </a-modal> 56 </a-modal>
47 57
48 - <!-- Webhook配置 -->  
49 <a-modal v-model:open="webhookVisible" title="Webhook配置" @ok="handleWebhookSave" :confirmLoading="saving"> 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 </a-modal> 78 </a-modal>
61 79
62 - <!-- 推送日志 -->  
63 <a-modal v-model:open="logsVisible" title="Webhook推送日志" :footer="null" width="800px"> 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 </template> 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 </a-modal> 99 </a-modal>
77 </div> 100 </div>
78 </template> 101 </template>
@@ -98,7 +121,7 @@ const columns = [ @@ -98,7 +121,7 @@ const columns = [
98 { title: 'ID', dataIndex: 'id', width: 80 }, 121 { title: 'ID', dataIndex: 'id', width: 80 },
99 { title: '应用名称', dataIndex: 'appName' }, 122 { title: '应用名称', dataIndex: 'appName' },
100 { title: 'AppKey', key: 'appKey' }, 123 { title: 'AppKey', key: 'appKey' },
101 - { title: '租户', dataIndex: 'cityId' }, 124 + { title: '租户', key: 'cityId' },
102 { title: '状态', key: 'status' }, 125 { title: '状态', key: 'status' },
103 { title: '回调地址', dataIndex: 'webhookUrl', ellipsis: true }, 126 { title: '回调地址', dataIndex: 'webhookUrl', ellipsis: true },
104 { title: '操作', key: 'action' }, 127 { title: '操作', key: 'action' },
@@ -127,6 +150,11 @@ async function loadCities() { @@ -127,6 +150,11 @@ async function loadCities() {
127 cityList.value = res.data 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 function openAdd() { 158 function openAdd() {
131 Object.assign(addForm, { appName: '', cityId: undefined, remark: '' }) 159 Object.assign(addForm, { appName: '', cityId: undefined, remark: '' })
132 addVisible.value = true 160 addVisible.value = true
src/views/open/OpenMockDelivery.vue
1 <template> 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 <a-button @click="fillDemo">填充示例</a-button> 10 <a-button @click="fillDemo">填充示例</a-button>
7 <a-button @click="resetForm">重置</a-button> 11 <a-button @click="resetForm">重置</a-button>
8 <a-button :loading="calcLoading" @click="handleCalcFee">按应用租户试算配送费</a-button> 12 <a-button :loading="calcLoading" @click="handleCalcFee">按应用租户试算配送费</a-button>
9 <a-button type="primary" :loading="saving" @click="handleSubmit">发送推单</a-button> 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 <a-form layout="vertical"> 29 <a-form layout="vertical">
14 <a-row :gutter="16"> 30 <a-row :gutter="16">
15 <a-col :xs="24" :lg="12"> 31 <a-col :xs="24" :lg="12">
@@ -47,100 +63,121 @@ @@ -47,100 +63,121 @@
47 </a-col> 63 </a-col>
48 </a-row> 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 </div> 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 <a-row 176 <a-row
140 v-for="(item, index) in form.items" 177 v-for="(item, index) in form.items"
141 :key="index" 178 :key="index"
142 :gutter="12" 179 :gutter="12"
143 - style="margin-bottom:12px;align-items:flex-start" 180 + class="mock-item-row"
144 > 181 >
145 <a-col :xs="24" :lg="6"> 182 <a-col :xs="24" :lg="6">
146 <a-form-item :label="index === 0 ? '名称' : ''"> 183 <a-form-item :label="index === 0 ? '名称' : ''">
@@ -168,18 +205,21 @@ @@ -168,18 +205,21 @@
168 </a-form-item> 205 </a-form-item>
169 </a-col> 206 </a-col>
170 </a-row> 207 </a-row>
  208 + </div>
  209 + <a-empty v-else description="暂无货物清单" />
171 </div> 210 </div>
172 - <a-empty v-else description="暂无货物清单" />  
173 </a-form> 211 </a-form>
  212 + </div>
174 </a-card> 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 <a-descriptions bordered :column="2"> 223 <a-descriptions bordered :column="2">
184 <a-descriptions-item label="配送费">¥{{ feeResult.totalFee }}</a-descriptions-item> 224 <a-descriptions-item label="配送费">¥{{ feeResult.totalFee }}</a-descriptions-item>
185 <a-descriptions-item label="配送距离">{{ feeResult.distance }} km</a-descriptions-item> 225 <a-descriptions-item label="配送距离">{{ feeResult.distance }} km</a-descriptions-item>
@@ -201,9 +241,15 @@ @@ -201,9 +241,15 @@
201 <a-descriptions-item label="计费重量">{{ feeResult.weight }} kg</a-descriptions-item> 241 <a-descriptions-item label="计费重量">{{ feeResult.weight }} kg</a-descriptions-item>
202 <a-descriptions-item label="计费件数">{{ feeResult.pieces || 0 }} 件</a-descriptions-item> 242 <a-descriptions-item label="计费件数">{{ feeResult.pieces || 0 }} 件</a-descriptions-item>
203 </a-descriptions> 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 <a-descriptions bordered :column="2"> 253 <a-descriptions bordered :column="2">
208 <a-descriptions-item label="配送单ID">{{ result.deliveryOrderId }}</a-descriptions-item> 254 <a-descriptions-item label="配送单ID">{{ result.deliveryOrderId }}</a-descriptions-item>
209 <a-descriptions-item label="平台订单号">{{ result.orderNo }}</a-descriptions-item> 255 <a-descriptions-item label="平台订单号">{{ result.orderNo }}</a-descriptions-item>
@@ -216,7 +262,8 @@ @@ -216,7 +262,8 @@
216 <a-descriptions-item label="时段附加费">¥{{ result.moneyTime }}</a-descriptions-item> 262 <a-descriptions-item label="时段附加费">¥{{ result.moneyTime }}</a-descriptions-item>
217 <a-descriptions-item label="预计送达">{{ result.estimatedMinutes }} 分钟</a-descriptions-item> 263 <a-descriptions-item label="预计送达">{{ result.estimatedMinutes }} 分钟</a-descriptions-item>
218 </a-descriptions> 264 </a-descriptions>
219 - </a-card> 265 + </a-card>
  266 + </div>
220 </div> 267 </div>
221 </template> 268 </template>
222 269
@@ -470,7 +517,17 @@ watch( @@ -470,7 +517,17 @@ watch(
470 517
471 <style scoped> 518 <style scoped>
472 .mock-delivery-page { 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 </style> 533 </style>
src/views/order/OrderList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option :value="2">已支付</a-select-option> 7 <a-select-option :value="2">已支付</a-select-option>
8 <a-select-option :value="3">已接单</a-select-option> 8 <a-select-option :value="3">已接单</a-select-option>
9 <a-select-option :value="4">服务中</a-select-option> 9 <a-select-option :value="4">服务中</a-select-option>
@@ -11,14 +11,14 @@ @@ -11,14 +11,14 @@
11 <a-select-option :value="7">退款申请</a-select-option> 11 <a-select-option :value="7">退款申请</a-select-option>
12 <a-select-option :value="10">已取消</a-select-option> 12 <a-select-option :value="10">已取消</a-select-option>
13 </a-select> 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 <a-select-option :value="2">转单申请中</a-select-option> 15 <a-select-option :value="2">转单申请中</a-select-option>
16 <a-select-option :value="1">已转单</a-select-option> 16 <a-select-option :value="1">已转单</a-select-option>
17 <a-select-option :value="3">转单拒绝</a-select-option> 17 <a-select-option :value="3">转单拒绝</a-select-option>
18 </a-select> 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 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 22 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
23 <template #bodyCell="{ column, record }"> 23 <template #bodyCell="{ column, record }">
24 <template v-if="column.key === 'status'"> 24 <template v-if="column.key === 'status'">
@@ -48,57 +48,72 @@ @@ -48,57 +48,72 @@
48 </a-table> 48 </a-table>
49 </a-card> 49 </a-card>
50 50
51 - <!-- 指派骑手弹窗 -->  
52 <a-modal v-model:open="designateVisible" title="指派骑手" @ok="handleDesignate" :confirmLoading="saving"> 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 </a-modal> 80 </a-modal>
76 81
77 - <!-- 退款记录弹窗 -->  
78 <a-modal v-model:open="refundVisible" title="退款记录" :footer="null"> 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 </a-modal> 107 </a-modal>
98 108
99 - <!-- 拒绝退款弹窗 -->  
100 <a-modal v-model:open="rejectVisible" title="拒绝退款" @ok="handleRefund(2)" :confirmLoading="saving"> 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 </a-modal> 117 </a-modal>
103 </div> 118 </div>
104 </template> 119 </template>
src/views/order/RefundList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option :value="0">待处理</a-select-option> 7 <a-select-option :value="0">待处理</a-select-option>
8 <a-select-option :value="1">已通过</a-select-option> 8 <a-select-option :value="1">已通过</a-select-option>
9 <a-select-option :value="2">已拒绝</a-select-option> 9 <a-select-option :value="2">已拒绝</a-select-option>
10 </a-select> 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 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 14 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
15 <template #bodyCell="{ column, record }"> 15 <template #bodyCell="{ column, record }">
16 <template v-if="column.key === 'role'"> 16 <template v-if="column.key === 'role'">
src/views/rider/RiderEvaluateList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option :value="0">全部</a-select-option> 8 <a-select-option :value="0">全部</a-select-option>
9 <a-select-option :value="1">好评(4-5星)</a-select-option> 9 <a-select-option :value="1">好评(4-5星)</a-select-option>
10 <a-select-option :value="2">中评(3星)</a-select-option> 10 <a-select-option :value="2">中评(3星)</a-select-option>
11 <a-select-option :value="3">差评(1-2星)</a-select-option> 11 <a-select-option :value="3">差评(1-2星)</a-select-option>
12 </a-select> 12 </a-select>
  13 + </div>
  14 + <div class="list-toolbar-right">
13 <a-button type="primary" @click="loadList">查询</a-button> 15 <a-button type="primary" @click="loadList">查询</a-button>
14 - </a-space>  
15 - </template> 16 + </div>
  17 + </div>
16 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 18 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
17 <template #bodyCell="{ column, record }"> 19 <template #bodyCell="{ column, record }">
  20 + <template v-if="column.key === 'cityId'">
  21 + {{ getCityName(record.cityId) }}
  22 + </template>
18 <template v-if="column.key === 'star'"> 23 <template v-if="column.key === 'star'">
19 <a-rate :value="record.star" disabled /> 24 <a-rate :value="record.star" disabled />
20 </template> 25 </template>
@@ -25,11 +30,12 @@ @@ -25,11 +30,12 @@
25 </template> 30 </template>
26 31
27 <script setup lang="ts"> 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 const loading = ref(false) 36 const loading = ref(false)
32 const list = ref<any[]>([]) 37 const list = ref<any[]>([])
  38 +const cityList = ref<any[]>([])
33 const filterRiderId = ref<number | undefined>() 39 const filterRiderId = ref<number | undefined>()
34 const filterType = ref(0) 40 const filterType = ref(0)
35 41
@@ -38,9 +44,19 @@ const columns = [ @@ -38,9 +44,19 @@ const columns = [
38 { title: '骑手ID', dataIndex: 'rid' }, 44 { title: '骑手ID', dataIndex: 'rid' },
39 { title: '评分', key: 'star' }, 45 { title: '评分', key: 'star' },
40 { title: '内容', dataIndex: 'content', ellipsis: true }, 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 async function loadList() { 60 async function loadList() {
45 if (!filterRiderId.value) return 61 if (!filterRiderId.value) return
46 loading.value = true 62 loading.value = true
@@ -49,4 +65,6 @@ async function loadList() { @@ -49,4 +65,6 @@ async function loadList() {
49 list.value = res.data || [] 65 list.value = res.data || []
50 } finally { loading.value = false } 66 } finally { loading.value = false }
51 } 67 }
  68 +
  69 +onMounted(loadCities)
52 </script> 70 </script>
src/views/rider/RiderList.vue
1 <template> 1 <template>
2 <div> 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 <a-select-option :value="2">待审核</a-select-option> 7 <a-select-option :value="2">待审核</a-select-option>
8 <a-select-option :value="1">已通过</a-select-option> 8 <a-select-option :value="1">已通过</a-select-option>
9 <a-select-option :value="0">已拒绝</a-select-option> 9 <a-select-option :value="0">已拒绝</a-select-option>
10 </a-select> 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 <a-button type="primary" @click="openAdd">新增骑手</a-button> 14 <a-button type="primary" @click="openAdd">新增骑手</a-button>
13 - </a-space>  
14 - </template> 15 + </div>
  16 + </div>
15 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 17 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
16 <template #bodyCell="{ column, record }"> 18 <template #bodyCell="{ column, record }">
17 <template v-if="column.key === 'userStatus'"> 19 <template v-if="column.key === 'userStatus'">
@@ -19,6 +21,9 @@ @@ -19,6 +21,9 @@
19 {{ statusMap[record.userStatus] }} 21 {{ statusMap[record.userStatus] }}
20 </a-tag> 22 </a-tag>
21 </template> 23 </template>
  24 + <template v-if="column.key === 'cityId'">
  25 + {{ getCityName(record.cityId) }}
  26 + </template>
22 <template v-if="column.key === 'levelName'"> 27 <template v-if="column.key === 'levelName'">
23 {{ record.levelName || '默认等级' }} 28 {{ record.levelName || '默认等级' }}
24 </template> 29 </template>
@@ -67,38 +72,50 @@ @@ -67,38 +72,50 @@
67 </a-card> 72 </a-card>
68 73
69 <a-modal v-model:open="modalVisible" title="新增骑手" @ok="handleAdd" :confirmLoading="saving"> 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 </a-modal> 97 </a-modal>
87 98
88 <a-modal v-model:open="levelVisible" title="设置骑手等级" @ok="handleSetLevel" :confirmLoading="levelSaving"> 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 </a-modal> 119 </a-modal>
103 </div> 120 </div>
104 </template> 121 </template>
@@ -137,7 +154,7 @@ const columns = [ @@ -137,7 +154,7 @@ const columns = [
137 { title: 'ID', dataIndex: 'id', width: 80 }, 154 { title: 'ID', dataIndex: 'id', width: 80 },
138 { title: '昵称', dataIndex: 'userNickname' }, 155 { title: '昵称', dataIndex: 'userNickname' },
139 { title: '手机', dataIndex: 'mobile' }, 156 { title: '手机', dataIndex: 'mobile' },
140 - { title: '租户ID', dataIndex: 'cityId' }, 157 + { title: '租户', key: 'cityId' },
141 { title: '等级', key: 'levelName' }, 158 { title: '等级', key: 'levelName' },
142 { title: '类型', key: 'type' }, 159 { title: '类型', key: 'type' },
143 { title: '审核状态', key: 'userStatus' }, 160 { title: '审核状态', key: 'userStatus' },
@@ -155,6 +172,11 @@ function getWorkStatus(record: any) { @@ -155,6 +172,11 @@ function getWorkStatus(record: any) {
155 return record.isRest === 1 ? 1 : 0 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 async function loadList() { 180 async function loadList() {
159 loading.value = true 181 loading.value = true
160 try { 182 try {
@@ -243,8 +265,6 @@ async function setType(riderId: number, type: number) { @@ -243,8 +265,6 @@ async function setType(riderId: number, type: number) {
243 265
244 onMounted(() => { 266 onMounted(() => {
245 loadList() 267 loadList()
246 - if (isAdmin.value) {  
247 - loadCities()  
248 - } 268 + loadCities()
249 }) 269 })
250 </script> 270 </script>
src/views/substation/SubstationList.vue
1 <template> 1 <template>
2 <div> 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 <a-button type="primary" @click="openAdd">新增分站</a-button> 9 <a-button type="primary" @click="openAdd">新增分站</a-button>
8 - </a-space>  
9 - </template> 10 + </div>
  11 + </div>
10 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false"> 12 <a-table :dataSource="list" :columns="columns" :loading="loading" rowKey="id" :pagination="false">
11 <template #bodyCell="{ column, record }"> 13 <template #bodyCell="{ column, record }">
  14 + <template v-if="column.key === 'cityId'">
  15 + {{ getCityName(record.cityId) }}
  16 + </template>
12 <template v-if="column.key === 'status'"> 17 <template v-if="column.key === 'status'">
13 <a-tag :color="record.userStatus === 1 ? 'green' : 'red'"> 18 <a-tag :color="record.userStatus === 1 ? 'green' : 'red'">
14 {{ record.userStatus === 1 ? '正常' : '禁用' }} 19 {{ record.userStatus === 1 ? '正常' : '禁用' }}
@@ -34,39 +39,49 @@ @@ -34,39 +39,49 @@
34 </a-table> 39 </a-table>
35 </a-card> 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 </a-modal> 68 </a-modal>
59 69
60 - <!-- 改密码弹窗 -->  
61 <a-modal v-model:open="pwdVisible" title="修改密码" @ok="handleChangePwd" :confirmLoading="pwdSaving"> 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 </a-modal> 85 </a-modal>
71 </div> 86 </div>
72 </template> 87 </template>
@@ -90,11 +105,16 @@ const columns = [ @@ -90,11 +105,16 @@ const columns = [
90 { title: '账号', dataIndex: 'userLogin' }, 105 { title: '账号', dataIndex: 'userLogin' },
91 { title: '昵称', dataIndex: 'userNickname' }, 106 { title: '昵称', dataIndex: 'userNickname' },
92 { title: '手机', dataIndex: 'mobile' }, 107 { title: '手机', dataIndex: 'mobile' },
93 - { title: '租户ID', dataIndex: 'cityId' }, 108 + { title: '租户', key: 'cityId' },
94 { title: '状态', key: 'status' }, 109 { title: '状态', key: 'status' },
95 { title: '操作', key: 'action' }, 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 async function loadList() { 118 async function loadList() {
99 loading.value = true 119 loading.value = true
100 try { 120 try {
@@ -169,7 +189,7 @@ async function handleChangePwd() { @@ -169,7 +189,7 @@ async function handleChangePwd() {
169 } 189 }
170 pwdSaving.value = true 190 pwdSaving.value = true
171 try { 191 try {
172 - await substationApi.changePassword(pwdForm) 192 + await substationApi.changePassword({ id: pwdTargetId.value, ...pwdForm })
173 message.success('密码修改成功') 193 message.success('密码修改成功')
174 pwdVisible.value = false 194 pwdVisible.value = false
175 } finally { pwdSaving.value = false } 195 } finally { pwdSaving.value = false }