Commit fdbd231e57745f4c37fb92a5c79eeb3ea668a1ab

Authored by 杨刚
1 parent 8a06965a

init

src/api/index.ts
... ... @@ -56,6 +56,8 @@ export const riderApi = {
56 56 request.post('/api/admin/rider/setEnableStatus', null, { params: { riderId, status } }),
57 57 setType: (riderId: number, type: number) =>
58 58 request.post('/api/admin/rider/setType', null, { params: { riderId, type } }),
  59 + designateCandidates: (orderId: number) =>
  60 + request.get('/api/admin/rider/order/candidates', { params: { orderId } }),
59 61 designate: (orderId: number, riderId: number) =>
60 62 request.post('/api/admin/rider/order/designate', null, { params: { orderId, riderId } }),
61 63 setTrans: (orderId: number, trans: number) =>
... ... @@ -90,6 +92,7 @@ export const refundApi = {
90 92 export const openApi = {
91 93 list: (page = 1) => request.get('/api/platform/open/list', { params: { page } }),
92 94 create: (data: any) => request.post('/api/platform/open/create', null, { params: data }),
  95 + mockDeliveryCreate: (data: any) => request.post('/api/platform/open/mockDelivery/create', data),
93 96 resetSecret: (appId: number) =>
94 97 request.post('/api/platform/open/resetSecret', null, { params: { appId } }),
95 98 setStatus: (appId: number, status: number) =>
... ...
src/layouts/MainLayout.vue
1 1 <template>
2   - <a-layout style="min-height: 100vh">
3   - <a-layout-sider v-model:collapsed="collapsed" collapsible>
4   - <div class="logo">{{ collapsed ? '地利' : '地利外卖管理' }}</div>
5   - <a-menu
6   - v-model:selectedKeys="selectedKeys"
7   - theme="dark"
8   - mode="inline"
9   - @click="onMenuClick"
10   - >
11   - <a-menu-item key="/city">
12   - <template #icon><global-outlined /></template>
13   - 城市管理
14   - </a-menu-item>
15   - <a-menu-item key="/substation">
16   - <template #icon><apartment-outlined /></template>
17   - 分站管理
18   - </a-menu-item>
19   - <a-sub-menu key="merchant">
20   - <template #icon><shop-outlined /></template>
21   - <template #title>商家管理</template>
22   - <a-menu-item key="/merchant/enter">入驻申请</a-menu-item>
23   - <a-menu-item key="/merchant/store">店铺管理</a-menu-item>
24   - </a-sub-menu>
25   - <a-menu-item key="/rider">
26   - <template #icon><user-outlined /></template>
27   - 骑手管理
28   - </a-menu-item>
29   - <a-menu-item key="/rider/evaluate">
30   - <template #icon><star-outlined /></template>
31   - 骑手评价
32   - </a-menu-item>
33   - <a-sub-menu key="orders">
34   - <template #icon><unordered-list-outlined /></template>
35   - <template #title>订单管理</template>
36   - <a-menu-item key="/order">订单列表</a-menu-item>
37   - <a-menu-item key="/refund">退款管理</a-menu-item>
38   - <a-menu-item key="/delivery/order">配送订单</a-menu-item>
39   - </a-sub-menu>
40   - <a-menu-item key="/open">
41   - <template #icon><api-outlined /></template>
42   - 开放平台
43   - </a-menu-item>
44   - </a-menu>
45   - </a-layout-sider>
46   - <a-layout>
47   - <a-layout-header style="background:#fff;padding:0 16px;display:flex;align-items:center;justify-content:flex-end">
48   - <a-dropdown>
49   - <a-button type="text">管理员 <down-outlined /></a-button>
50   - <template #overlay>
51   - <a-menu>
52   - <a-menu-item @click="handleLogout">退出登录</a-menu-item>
53   - </a-menu>
54   - </template>
55   - </a-dropdown>
56   - </a-layout-header>
57   - <a-layout-content style="margin:16px">
  2 + <div class="layout-shell">
  3 + <aside class="soft-sider" :class="{ collapsed }">
  4 + <button class="sider-toggle" type="button" @click="collapsed = !collapsed">
  5 + <menu-fold-outlined v-if="!collapsed" />
  6 + <menu-unfold-outlined v-else />
  7 + </button>
  8 +
  9 + <div class="brand-block">
  10 + <div class="brand-mark">DL</div>
  11 + <div v-if="!collapsed" class="brand-copy">
  12 + <strong>地利骑手中台</strong>
  13 + <span>Soft operations cockpit</span>
  14 + </div>
  15 + </div>
  16 +
  17 + <div class="menu-scroll">
  18 + <a-menu
  19 + v-model:selectedKeys="selectedKeys"
  20 + mode="inline"
  21 + @click="onMenuClick"
  22 + >
  23 + <a-menu-item key="/dashboard">
  24 + <template #icon><home-outlined /></template>
  25 + 工作台
  26 + </a-menu-item>
  27 + <a-menu-item key="/city">
  28 + <template #icon><global-outlined /></template>
  29 + 租户管理
  30 + </a-menu-item>
  31 + <a-menu-item key="/substation">
  32 + <template #icon><apartment-outlined /></template>
  33 + 分站管理
  34 + </a-menu-item>
  35 + <a-sub-menu key="merchant">
  36 + <template #icon><shop-outlined /></template>
  37 + <template #title>商家管理</template>
  38 + <a-menu-item key="/merchant/enter">入驻申请</a-menu-item>
  39 + <a-menu-item key="/merchant/store">店铺管理</a-menu-item>
  40 + </a-sub-menu>
  41 + <a-menu-item key="/rider">
  42 + <template #icon><user-outlined /></template>
  43 + 骑手管理
  44 + </a-menu-item>
  45 + <a-menu-item key="/rider/evaluate">
  46 + <template #icon><star-outlined /></template>
  47 + 骑手评价
  48 + </a-menu-item>
  49 + <a-sub-menu key="orders">
  50 + <template #icon><unordered-list-outlined /></template>
  51 + <template #title>订单管理</template>
  52 + <a-menu-item key="/order">订单列表</a-menu-item>
  53 + <a-menu-item key="/refund">退款管理</a-menu-item>
  54 + <a-menu-item key="/delivery/order">配送订单</a-menu-item>
  55 + </a-sub-menu>
  56 + <a-sub-menu key="open">
  57 + <template #icon><api-outlined /></template>
  58 + <template #title>开放平台</template>
  59 + <a-menu-item key="/open">应用管理</a-menu-item>
  60 + <a-menu-item key="/open/mock-delivery">模拟推单</a-menu-item>
  61 + </a-sub-menu>
  62 + </a-menu>
  63 + </div>
  64 +
  65 + <div v-if="!collapsed" class="sider-foot">
  66 + <div class="soft-chip soft-chip-green">在线协作</div>
  67 + <p>面向运营、分站与骑手管理的统一轻量工作台。</p>
  68 + </div>
  69 + </aside>
  70 +
  71 + <main class="content-column">
  72 + <header class="soft-topbar">
  73 + <div>
  74 + <p class="eyebrow">Soft-Neo Admin</p>
  75 + <h1>{{ currentTitle }}</h1>
  76 + </div>
  77 + <div class="topbar-actions">
  78 + <div class="date-pill">
  79 + <calendar-outlined />
  80 + <span>{{ todayLabel }}</span>
  81 + </div>
  82 + <a-dropdown>
  83 + <a-button type="text" class="profile-button">
  84 + <span class="profile-avatar">{{ avatarText }}</span>
  85 + <span class="profile-copy">
  86 + <strong>{{ auth.userInfo?.userNickname || '管理员' }}</strong>
  87 + <small>{{ auth.userInfo?.role === 'admin' ? '超级管理员' : '分站管理员' }}</small>
  88 + </span>
  89 + <down-outlined />
  90 + </a-button>
  91 + <template #overlay>
  92 + <a-menu>
  93 + <a-menu-item @click="handleLogout">退出登录</a-menu-item>
  94 + </a-menu>
  95 + </template>
  96 + </a-dropdown>
  97 + </div>
  98 + </header>
  99 +
  100 + <div class="soft-page-shell">
58 101 <router-view />
59   - </a-layout-content>
60   - </a-layout>
61   - </a-layout>
  102 + </div>
  103 + </main>
  104 + </div>
62 105 </template>
63 106  
64 107 <script setup lang="ts">
65   -import { ref, watch } from 'vue'
  108 +import { computed, ref, watch } from 'vue'
66 109 import { useRouter, useRoute } from 'vue-router'
67 110 import { useAuthStore } from '@/stores/auth'
68 111 import {
69 112 GlobalOutlined, ApartmentOutlined, ShopOutlined,
70   - UserOutlined, UnorderedListOutlined, ApiOutlined, DownOutlined, StarOutlined
  113 + UserOutlined, UnorderedListOutlined, ApiOutlined, DownOutlined, StarOutlined,
  114 + CalendarOutlined, MenuFoldOutlined, MenuUnfoldOutlined, HomeOutlined
71 115 } from '@ant-design/icons-vue'
72 116  
73 117 const router = useRouter()
... ... @@ -75,9 +119,31 @@ const route = useRoute()
75 119 const auth = useAuthStore()
76 120 const collapsed = ref(false)
77 121 const selectedKeys = ref([route.path])
  122 +const titleMap: Record<string, string> = {
  123 + '/dashboard': '工作台',
  124 + '/city': '租户管理',
  125 + '/substation': '分站管理',
  126 + '/merchant/enter': '商家入驻',
  127 + '/merchant/store': '店铺管理',
  128 + '/rider': '骑手管理',
  129 + '/rider/evaluate': '骑手评价',
  130 + '/order': '订单列表',
  131 + '/refund': '退款管理',
  132 + '/delivery/order': '配送订单',
  133 + '/open': '开放平台',
  134 + '/open/mock-delivery': '模拟推单',
  135 +}
78 136  
79 137 watch(() => route.path, (p) => { selectedKeys.value = [p] })
80 138  
  139 +const currentTitle = computed(() => titleMap[route.path] || '外卖管理')
  140 +const avatarText = computed(() => (auth.userInfo?.userNickname || '管理员').slice(0, 1))
  141 +const todayLabel = computed(() => new Intl.DateTimeFormat('zh-CN', {
  142 + month: 'long',
  143 + day: 'numeric',
  144 + weekday: 'short',
  145 +}).format(new Date()))
  146 +
81 147 function onMenuClick({ key }: { key: string }) {
82 148 router.push(key)
83 149 }
... ... @@ -89,15 +155,234 @@ function handleLogout() {
89 155 </script>
90 156  
91 157 <style scoped>
92   -.logo {
93   - height: 64px;
94   - color: #fff;
95   - font-size: 16px;
96   - font-weight: bold;
  158 +.layout-shell {
  159 + min-height: 100vh;
  160 + display: grid;
  161 + grid-template-columns: 280px minmax(0, 1fr);
  162 + gap: 22px;
  163 + padding: 22px;
  164 +}
  165 +
  166 +.soft-sider,
  167 +.soft-topbar {
  168 + border: 1px solid rgba(255, 255, 255, 0.58);
  169 + background: rgba(255, 255, 255, 0.74);
  170 + backdrop-filter: blur(22px);
  171 + box-shadow: 0 16px 40px rgba(130, 110, 218, 0.12);
  172 +}
  173 +
  174 +.soft-sider {
  175 + position: sticky;
  176 + top: 22px;
  177 + height: calc(100vh - 44px);
  178 + border-radius: 34px;
  179 + padding: 18px 14px 18px;
97 180 display: flex;
98   - align-items: center;
99   - justify-content: center;
  181 + flex-direction: column;
100 182 overflow: hidden;
101   - white-space: nowrap;
  183 +}
  184 +
  185 +.menu-scroll {
  186 + flex: 1;
  187 + min-height: 0;
  188 + overflow-y: auto;
  189 + overflow-x: hidden;
  190 + padding-right: 4px;
  191 +}
  192 +
  193 +.menu-scroll::-webkit-scrollbar {
  194 + width: 8px;
  195 +}
  196 +
  197 +.menu-scroll::-webkit-scrollbar-thumb {
  198 + border-radius: 999px;
  199 + background: rgba(140, 124, 240, 0.24);
  200 +}
  201 +
  202 +.soft-sider.collapsed {
  203 + width: 88px;
  204 +}
  205 +
  206 +.sider-toggle {
  207 + align-self: flex-end;
  208 + width: 44px;
  209 + height: 44px;
  210 + border-radius: 16px;
  211 + border: none;
  212 + background: rgba(246, 242, 255, 0.9);
  213 + color: #7f6de5;
  214 + cursor: pointer;
  215 + margin-bottom: 12px;
  216 +}
  217 +
  218 +.brand-block {
  219 + display: flex;
  220 + align-items: center;
  221 + gap: 14px;
  222 + padding: 10px 10px 20px;
  223 +}
  224 +
  225 +.brand-mark {
  226 + width: 52px;
  227 + height: 52px;
  228 + border-radius: 18px;
  229 + background: linear-gradient(145deg, #8c7cf0, #e6b5dc);
  230 + color: white;
  231 + font-family: 'Outfit', sans-serif;
  232 + font-weight: 700;
  233 + display: grid;
  234 + place-items: center;
  235 + box-shadow: 0 14px 24px rgba(140, 124, 240, 0.28);
  236 +}
  237 +
  238 +.brand-copy {
  239 + display: flex;
  240 + flex-direction: column;
  241 +}
  242 +
  243 +.brand-copy strong,
  244 +.profile-copy strong,
  245 +.hero-copy h2,
  246 +.insight-card strong,
  247 +h1 {
  248 + font-family: 'Outfit', sans-serif;
  249 + color: #2f2946;
  250 +}
  251 +
  252 +.brand-copy span,
  253 +.profile-copy small,
  254 +.eyebrow,
  255 +.hero-copy p,
  256 +.insight-card p,
  257 +.note-list {
  258 + color: #8d88a4;
  259 +}
  260 +
  261 +:deep(.ant-menu) {
  262 + min-height: 100%;
  263 + background: transparent;
  264 +}
  265 +
  266 +.sider-foot {
  267 + margin-top: 14px;
  268 + flex-shrink: 0;
  269 + border-radius: 24px;
  270 + background: linear-gradient(180deg, rgba(245, 241, 255, 0.85), rgba(255, 247, 250, 0.92));
  271 + padding: 16px;
  272 +}
  273 +
  274 +.soft-chip {
  275 + display: inline-flex;
  276 + align-items: center;
  277 + border-radius: 999px;
  278 + padding: 6px 12px;
  279 + font-size: 12px;
  280 + font-weight: 700;
  281 +}
  282 +
  283 +.soft-chip-green {
  284 + background: rgba(139, 212, 167, 0.22);
  285 + color: #3d8f63;
  286 +}
  287 +
  288 +.content-column {
  289 + min-width: 0;
  290 +}
  291 +
  292 +.soft-topbar {
  293 + border-radius: 30px;
  294 + padding: 18px 24px;
  295 + display: flex;
  296 + align-items: center;
  297 + justify-content: space-between;
  298 + gap: 18px;
  299 +}
  300 +
  301 +.soft-topbar h1 {
  302 + margin: 4px 0 0;
  303 + font-size: 30px;
  304 +}
  305 +
  306 +.eyebrow {
  307 + margin: 0;
  308 + text-transform: uppercase;
  309 + letter-spacing: 0.12em;
  310 + font-size: 11px;
  311 + font-weight: 700;
  312 +}
  313 +
  314 +.topbar-actions {
  315 + display: flex;
  316 + align-items: center;
  317 + gap: 14px;
  318 +}
  319 +
  320 +.date-pill,
  321 +.profile-button {
  322 + display: inline-flex;
  323 + align-items: center;
  324 + gap: 10px;
  325 + border-radius: 999px;
  326 + background: rgba(255, 255, 255, 0.82);
  327 + border: 1px solid rgba(194, 184, 237, 0.38);
  328 + padding: 10px 14px;
  329 +}
  330 +
  331 +.profile-button {
  332 + height: auto;
  333 + padding-right: 12px;
  334 +}
  335 +
  336 +.profile-avatar {
  337 + width: 38px;
  338 + height: 38px;
  339 + border-radius: 14px;
  340 + display: grid;
  341 + place-items: center;
  342 + background: linear-gradient(135deg, #a48ef4, #f1bfd8);
  343 + color: white;
  344 + font-weight: 700;
  345 +}
  346 +
  347 +.profile-copy {
  348 + display: flex;
  349 + flex-direction: column;
  350 + text-align: left;
  351 +}
  352 +
  353 +@media (max-width: 960px) {
  354 + .layout-shell {
  355 + grid-template-columns: 1fr;
  356 + padding: 14px;
  357 + }
  358 +
  359 + .soft-sider {
  360 + position: relative;
  361 + top: 0;
  362 + height: auto;
  363 + }
  364 +
  365 + .menu-scroll {
  366 + overflow: visible;
  367 + padding-right: 0;
  368 + }
  369 + .soft-topbar {
  370 + flex-direction: column;
  371 + align-items: flex-start;
  372 + }
  373 +}
  374 +
  375 +@media (max-width: 720px) {
  376 + .soft-sider.collapsed {
  377 + width: auto;
  378 + }
  379 +
  380 + .soft-topbar h1 {
  381 + font-size: 26px;
  382 + }
  383 +
  384 + .hero-copy h2 {
  385 + font-size: 28px;
  386 + }
102 387 }
103 388 </style>
... ...
src/main.ts
... ... @@ -4,6 +4,7 @@ import Antd from &#39;ant-design-vue&#39;
4 4 import 'ant-design-vue/dist/reset.css'
5 5 import App from './App.vue'
6 6 import router from './router'
  7 +import './style.css'
7 8  
8 9 const app = createApp(App)
9 10 app.use(createPinia())
... ...
src/router/index.ts
... ... @@ -13,13 +13,19 @@ const router = createRouter({
13 13 {
14 14 path: '/',
15 15 component: () => import('@/layouts/MainLayout.vue'),
16   - redirect: '/city',
  16 + redirect: '/dashboard',
17 17 children: [
18 18 {
  19 + path: 'dashboard',
  20 + name: 'Dashboard',
  21 + component: () => import('@/views/dashboard/DashboardHome.vue'),
  22 + meta: { title: '工作台' },
  23 + },
  24 + {
19 25 path: 'city',
20 26 name: 'City',
21 27 component: () => import('@/views/city/CityList.vue'),
22   - meta: { title: '城市管理' },
  28 + meta: { title: '租户管理' },
23 29 },
24 30 {
25 31 path: 'substation',
... ... @@ -75,6 +81,12 @@ const router = createRouter({
75 81 component: () => import('@/views/open/OpenAppList.vue'),
76 82 meta: { title: '开放平台' },
77 83 },
  84 + {
  85 + path: 'open/mock-delivery',
  86 + name: 'OpenMockDelivery',
  87 + component: () => import('@/views/open/OpenMockDelivery.vue'),
  88 + meta: { title: '模拟推单' },
  89 + },
78 90 ],
79 91 },
80 92 { path: '/:pathMatch(.*)*', redirect: '/' },
... ...
src/style.css
  1 +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
  2 +
1 3 :root {
2   - --text: #6b6375;
3   - --text-h: #08060d;
4   - --bg: #fff;
5   - --border: #e5e4e7;
6   - --code-bg: #f4f3ec;
7   - --accent: #aa3bff;
8   - --accent-bg: rgba(170, 59, 255, 0.1);
9   - --accent-border: rgba(170, 59, 255, 0.5);
10   - --social-bg: rgba(244, 243, 236, 0.5);
11   - --shadow:
12   - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
13   -
14   - --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
15   - --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
16   - --mono: ui-monospace, Consolas, monospace;
17   -
18   - font: 18px/145% var(--sans);
19   - letter-spacing: 0.18px;
20   - color-scheme: light dark;
21   - color: var(--text);
22   - background: var(--bg);
  4 + --app-bg:
  5 + radial-gradient(circle at top left, rgba(198, 185, 255, 0.58), transparent 28%),
  6 + radial-gradient(circle at 85% 18%, rgba(255, 217, 236, 0.7), transparent 24%),
  7 + radial-gradient(circle at 80% 84%, rgba(255, 234, 181, 0.45), transparent 22%),
  8 + linear-gradient(180deg, #fbfaff 0%, #f7f7fd 48%, #f8f8fb 100%);
  9 + --panel: rgba(255, 255, 255, 0.76);
  10 + --panel-strong: rgba(255, 255, 255, 0.9);
  11 + --panel-tint: linear-gradient(135deg, rgba(198, 185, 255, 0.72), rgba(255, 218, 238, 0.72));
  12 + --line: rgba(182, 172, 226, 0.24);
  13 + --line-strong: rgba(140, 124, 240, 0.22);
  14 + --text-main: #4f4a68;
  15 + --text-soft: #8d88a4;
  16 + --text-dark: #2f2946;
  17 + --brand: #8c7cf0;
  18 + --brand-soft: #c6b9ff;
  19 + --brand-deep: #7563df;
  20 + --pink: #f4bfd8;
  21 + --yellow: #f6d977;
  22 + --green: #8bd4a7;
  23 + --orange: #ffb284;
  24 + --shadow-xl: 0 22px 60px rgba(121, 104, 213, 0.12);
  25 + --shadow-lg: 0 16px 35px rgba(137, 123, 214, 0.12);
  26 + --shadow-sm: 0 10px 20px rgba(149, 136, 220, 0.08);
  27 + --radius-xl: 32px;
  28 + --radius-lg: 24px;
  29 + --radius-md: 18px;
  30 + --radius-sm: 14px;
  31 + --font-display: 'Outfit', 'Avenir Next', 'Segoe UI', sans-serif;
  32 + --font-body: 'Plus Jakarta Sans', 'Segoe UI', sans-serif;
  33 + color: var(--text-main);
  34 + font-family: var(--font-body);
  35 + line-height: 1.5;
  36 + font-weight: 500;
23 37 font-synthesis: none;
24 38 text-rendering: optimizeLegibility;
25 39 -webkit-font-smoothing: antialiased;
26 40 -moz-osx-font-smoothing: grayscale;
27   -
28   - @media (max-width: 1024px) {
29   - font-size: 16px;
30   - }
  41 + background: #f8f8ff;
31 42 }
32 43  
33   -@media (prefers-color-scheme: dark) {
34   - :root {
35   - --text: #9ca3af;
36   - --text-h: #f3f4f6;
37   - --bg: #16171d;
38   - --border: #2e303a;
39   - --code-bg: #1f2028;
40   - --accent: #c084fc;
41   - --accent-bg: rgba(192, 132, 252, 0.15);
42   - --accent-border: rgba(192, 132, 252, 0.5);
43   - --social-bg: rgba(47, 48, 58, 0.5);
44   - --shadow:
45   - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
46   - }
  44 +* {
  45 + box-sizing: border-box;
  46 +}
47 47  
48   - #social .button-icon {
49   - filter: invert(1) brightness(2);
50   - }
  48 +html,
  49 +body,
  50 +#app {
  51 + min-height: 100vh;
51 52 }
52 53  
53 54 body {
54 55 margin: 0;
  56 + background: var(--app-bg);
  57 + color: var(--text-main);
55 58 }
56 59  
57   -h1,
58   -h2 {
59   - font-family: var(--heading);
60   - font-weight: 500;
61   - color: var(--text-h);
  60 +a {
  61 + color: var(--brand-deep);
62 62 }
63 63  
64   -h1 {
65   - font-size: 56px;
66   - letter-spacing: -1.68px;
67   - margin: 32px 0;
68   - @media (max-width: 1024px) {
69   - font-size: 36px;
70   - margin: 20px 0;
71   - }
72   -}
73   -h2 {
74   - font-size: 24px;
75   - line-height: 118%;
76   - letter-spacing: -0.24px;
77   - margin: 0 0 8px;
78   - @media (max-width: 1024px) {
79   - font-size: 20px;
80   - }
81   -}
82   -p {
83   - margin: 0;
  64 +.ant-layout {
  65 + background: transparent;
84 66 }
85 67  
86   -code,
87   -.counter {
88   - font-family: var(--mono);
89   - display: inline-flex;
90   - border-radius: 4px;
91   - color: var(--text-h);
  68 +.ant-btn {
  69 + border-radius: 999px;
  70 + font-weight: 600;
  71 + box-shadow: none;
92 72 }
93 73  
94   -code {
95   - font-size: 15px;
96   - line-height: 135%;
97   - padding: 4px 8px;
98   - background: var(--code-bg);
  74 +.ant-btn-primary {
  75 + background: linear-gradient(135deg, #8c7cf0 0%, #c995ea 100%);
  76 + border: none;
  77 + box-shadow: 0 12px 24px rgba(140, 124, 240, 0.24);
99 78 }
100 79  
101   -.counter {
102   - font-size: 16px;
103   - padding: 5px 10px;
104   - border-radius: 5px;
105   - color: var(--accent);
106   - background: var(--accent-bg);
107   - border: 2px solid transparent;
108   - transition: border-color 0.3s;
109   - margin-bottom: 24px;
  80 +.ant-btn-primary:hover,
  81 +.ant-btn-primary:focus {
  82 + background: linear-gradient(135deg, #7d6be8 0%, #c289ea 100%) !important;
  83 +}
110 84  
111   - &:hover {
112   - border-color: var(--accent-border);
113   - }
114   - &:focus-visible {
115   - outline: 2px solid var(--accent);
116   - outline-offset: 2px;
117   - }
  85 +.ant-btn-default {
  86 + border-color: var(--line);
  87 + background: rgba(255, 255, 255, 0.82);
118 88 }
119 89  
120   -.hero {
121   - position: relative;
  90 +.ant-card {
  91 + border: 1px solid rgba(255, 255, 255, 0.55);
  92 + background: var(--panel);
  93 + backdrop-filter: blur(18px);
  94 + border-radius: var(--radius-lg);
  95 + box-shadow: var(--shadow-lg);
  96 +}
122 97  
123   - .base,
124   - .framework,
125   - .vite {
126   - inset-inline: 0;
127   - margin: 0 auto;
128   - }
  98 +.ant-card .ant-card-head {
  99 + border-bottom: 1px solid rgba(188, 180, 230, 0.18);
  100 + min-height: 72px;
  101 +}
129 102  
130   - .base {
131   - width: 170px;
132   - position: relative;
133   - z-index: 0;
134   - }
  103 +.ant-card .ant-card-head-title {
  104 + font-family: var(--font-display);
  105 + font-size: 1.1rem;
  106 + color: var(--text-dark);
  107 + font-weight: 700;
  108 +}
135 109  
136   - .framework,
137   - .vite {
138   - position: absolute;
139   - }
  110 +.ant-card .ant-card-body {
  111 + padding: 24px;
  112 +}
140 113  
141   - .framework {
142   - z-index: 1;
143   - top: 34px;
144   - height: 28px;
145   - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
146   - scale(1.4);
147   - }
  114 +.ant-table-wrapper .ant-table {
  115 + background: transparent;
  116 +}
148 117  
149   - .vite {
150   - z-index: 0;
151   - top: 107px;
152   - height: 26px;
153   - width: auto;
154   - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
155   - scale(0.8);
156   - }
  118 +.ant-table-wrapper .ant-table-container {
  119 + border-radius: var(--radius-md);
  120 + border: 1px solid rgba(194, 185, 239, 0.22);
  121 + overflow: hidden;
157 122 }
158 123  
159   -#app {
160   - width: 1126px;
161   - max-width: 100%;
162   - margin: 0 auto;
163   - text-align: center;
164   - border-inline: 1px solid var(--border);
165   - min-height: 100svh;
166   - display: flex;
167   - flex-direction: column;
168   - box-sizing: border-box;
  124 +.ant-table-wrapper .ant-table-thead > tr > th {
  125 + background: rgba(244, 240, 255, 0.88);
  126 + color: var(--text-dark);
  127 + border-bottom: none;
  128 + font-weight: 700;
169 129 }
170 130  
171   -#center {
172   - display: flex;
173   - flex-direction: column;
174   - gap: 25px;
175   - place-content: center;
176   - place-items: center;
177   - flex-grow: 1;
  131 +.ant-table-wrapper .ant-table-tbody > tr > td {
  132 + border-bottom: 1px solid rgba(226, 220, 247, 0.72);
  133 + background: rgba(255, 255, 255, 0.48);
  134 +}
178 135  
179   - @media (max-width: 1024px) {
180   - padding: 32px 20px 24px;
181   - gap: 18px;
182   - }
  136 +.ant-table-wrapper .ant-table-tbody > tr:hover > td {
  137 + background: rgba(247, 242, 255, 0.95) !important;
183 138 }
184 139  
185   -#next-steps {
186   - display: flex;
187   - border-top: 1px solid var(--border);
188   - text-align: left;
  140 +.ant-input,
  141 +.ant-input-affix-wrapper,
  142 +.ant-input-password,
  143 +.ant-select-selector,
  144 +.ant-input-number,
  145 +.ant-input-number-input-wrap,
  146 +.ant-picker {
  147 + border-radius: 16px !important;
  148 + border-color: rgba(189, 180, 234, 0.4) !important;
  149 + background: rgba(255, 255, 255, 0.78) !important;
  150 + box-shadow: none !important;
  151 +}
189 152  
190   - & > div {
191   - flex: 1 1 0;
192   - padding: 32px;
193   - @media (max-width: 1024px) {
194   - padding: 24px 20px;
195   - }
196   - }
  153 +.ant-input:focus,
  154 +.ant-input-affix-wrapper-focused,
  155 +.ant-select-focused .ant-select-selector,
  156 +.ant-input-number-focused,
  157 +.ant-picker-focused {
  158 + border-color: rgba(140, 124, 240, 0.68) !important;
  159 + box-shadow: 0 0 0 4px rgba(140, 124, 240, 0.12) !important;
  160 +}
197 161  
198   - .icon {
199   - margin-bottom: 16px;
200   - width: 22px;
201   - height: 22px;
202   - }
  162 +.ant-modal .ant-modal-content,
  163 +.ant-dropdown .ant-dropdown-menu {
  164 + border-radius: 28px;
  165 + border: 1px solid rgba(228, 223, 247, 0.7);
  166 + background: rgba(255, 255, 255, 0.92);
  167 + backdrop-filter: blur(24px);
  168 + box-shadow: var(--shadow-xl);
  169 +}
203 170  
204   - @media (max-width: 1024px) {
205   - flex-direction: column;
206   - text-align: center;
207   - }
  171 +.ant-modal .ant-modal-header {
  172 + background: transparent;
  173 + border-bottom: 1px solid rgba(189, 180, 234, 0.18);
208 174 }
209 175  
210   -#docs {
211   - border-right: 1px solid var(--border);
  176 +.ant-modal .ant-modal-title {
  177 + font-family: var(--font-display);
  178 + color: var(--text-dark);
  179 + font-weight: 700;
  180 +}
212 181  
213   - @media (max-width: 1024px) {
214   - border-right: none;
215   - border-bottom: 1px solid var(--border);
216   - }
  182 +.ant-tag {
  183 + border-radius: 999px;
  184 + border: none;
  185 + padding-inline: 10px;
  186 + font-weight: 600;
217 187 }
218 188  
219   -#next-steps ul {
220   - list-style: none;
221   - padding: 0;
222   - display: flex;
223   - gap: 8px;
224   - margin: 32px 0 0;
  189 +.ant-menu {
  190 + background: transparent !important;
  191 +}
225 192  
226   - .logo {
227   - height: 18px;
228   - }
  193 +.ant-menu-item,
  194 +.ant-menu-submenu-title {
  195 + border-radius: 18px !important;
  196 + margin-inline: 8px !important;
  197 + margin-block: 6px !important;
  198 + width: calc(100% - 16px) !important;
  199 +}
229 200  
230   - a {
231   - color: var(--text-h);
232   - font-size: 16px;
233   - border-radius: 6px;
234   - background: var(--social-bg);
235   - display: flex;
236   - padding: 6px 12px;
237   - align-items: center;
238   - gap: 8px;
239   - text-decoration: none;
240   - transition: box-shadow 0.3s;
241   -
242   - &:hover {
243   - box-shadow: var(--shadow);
244   - }
245   - .button-icon {
246   - height: 18px;
247   - width: 18px;
248   - }
249   - }
  201 +.ant-menu-light .ant-menu-item-selected,
  202 +.ant-menu-light > .ant-menu .ant-menu-item-selected,
  203 +.ant-menu-light .ant-menu-submenu-selected > .ant-menu-submenu-title {
  204 + background: linear-gradient(135deg, rgba(140, 124, 240, 0.18), rgba(255, 212, 235, 0.3)) !important;
  205 + color: var(--brand-deep) !important;
  206 +}
250 207  
251   - @media (max-width: 1024px) {
252   - margin-top: 20px;
253   - flex-wrap: wrap;
254   - justify-content: center;
  208 +.ant-menu-item:hover,
  209 +.ant-menu-submenu-title:hover {
  210 + color: var(--brand-deep) !important;
  211 + background: rgba(255, 255, 255, 0.5) !important;
  212 +}
255 213  
256   - li {
257   - flex: 1 1 calc(50% - 8px);
258   - }
  214 +.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled) {
  215 + color: var(--brand-deep);
  216 + background: rgba(255, 255, 255, 0.92);
  217 + border-color: rgba(140, 124, 240, 0.45);
  218 + box-shadow: 0 8px 18px rgba(140, 124, 240, 0.12);
  219 +}
259 220  
260   - a {
261   - width: 100%;
262   - justify-content: center;
263   - box-sizing: border-box;
264   - }
265   - }
  221 +.ant-empty {
  222 + padding: 20px 0;
266 223 }
267 224  
268   -#spacer {
269   - height: 88px;
270   - border-top: 1px solid var(--border);
271   - @media (max-width: 1024px) {
272   - height: 48px;
273   - }
  225 +.soft-page-shell {
  226 + padding: 24px 24px 28px;
274 227 }
275 228  
276   -.ticks {
  229 +.soft-section-card {
277 230 position: relative;
278   - width: 100%;
279   -
280   - &::before,
281   - &::after {
282   - content: '';
283   - position: absolute;
284   - top: -4.5px;
285   - border: 5px solid transparent;
286   - }
  231 + overflow: hidden;
  232 +}
287 233  
288   - &::before {
289   - left: 0;
290   - border-left-color: var(--border);
291   - }
292   - &::after {
293   - right: 0;
294   - border-right-color: var(--border);
  234 +.soft-section-card::before {
  235 + content: '';
  236 + position: absolute;
  237 + inset: 0 auto auto 0;
  238 + width: 180px;
  239 + height: 180px;
  240 + background: radial-gradient(circle, rgba(198, 185, 255, 0.35) 0%, transparent 70%);
  241 + pointer-events: none;
  242 +}
  243 +
  244 +@media (max-width: 1200px) {
  245 + .soft-page-shell {
  246 + padding: 18px;
295 247 }
296 248 }
... ...
src/views/Login.vue
1 1 <template>
2 2 <div class="login-wrap">
3   - <a-card title="外卖管理系统" style="width:400px">
4   - <a-form :model="form" @finish="onSubmit" layout="vertical">
5   - <a-form-item name="role" label="登录身份">
6   - <a-radio-group v-model:value="form.role" button-style="solid">
7   - <a-radio-button value="substation">分站管理员</a-radio-button>
8   - <a-radio-button value="admin">超级管理员</a-radio-button>
9   - </a-radio-group>
10   - </a-form-item>
11   - <a-form-item name="account" :rules="[{ required: true, message: '请输入账号' }]">
12   - <a-input v-model:value="form.account" placeholder="登录账号" size="large">
13   - <template #prefix><user-outlined /></template>
14   - </a-input>
15   - </a-form-item>
16   - <a-form-item name="pass" :rules="[{ required: true, message: '请输入密码' }]">
17   - <a-input-password v-model:value="form.pass" placeholder="密码" size="large">
18   - <template #prefix><lock-outlined /></template>
19   - </a-input-password>
20   - </a-form-item>
21   - <a-form-item>
22   - <a-button type="primary" html-type="submit" block size="large" :loading="loading">
23   - 登录
24   - </a-button>
25   - </a-form-item>
26   - </a-form>
27   - </a-card>
  3 + <div class="login-shell">
  4 + <section class="login-visual">
  5 + <div class="visual-badge">Modern Soft-Neo UI</div>
  6 + <h1>让配送管理像清晨的云层一样轻盈。</h1>
  7 + <p>柔和渐变、圆润卡片与轻插画氛围,把后台工作台重新整理成更亲和的中控体验。</p>
  8 + <div class="visual-stats">
  9 + <div>
  10 + <span>Theme</span>
  11 + <strong>Lavender + Warm Glow</strong>
  12 + </div>
  13 + <div>
  14 + <span>Layout</span>
  15 + <strong>Floating Three-Column</strong>
  16 + </div>
  17 + </div>
  18 + <div class="illustration-wrap">
  19 + <div class="bubble bubble-a"></div>
  20 + <div class="bubble bubble-b"></div>
  21 + <img :src="heroImage" alt="soft illustration" />
  22 + </div>
  23 + </section>
  24 +
  25 + <a-card class="login-card">
  26 + <div class="login-card-head">
  27 + <span class="head-chip">Welcome Back</span>
  28 + <h2>外卖管理系统</h2>
  29 + <p>登录到柔和重构后的运营工作台</p>
  30 + </div>
  31 + <a-form :model="form" @finish="onSubmit" layout="vertical">
  32 + <a-form-item name="role" label="登录身份">
  33 + <a-radio-group v-model:value="form.role" button-style="solid">
  34 + <a-radio-button value="substation">分站管理员</a-radio-button>
  35 + <a-radio-button value="admin">超级管理员</a-radio-button>
  36 + </a-radio-group>
  37 + </a-form-item>
  38 + <a-form-item name="account" :rules="[{ required: true, message: '请输入账号' }]">
  39 + <a-input v-model:value="form.account" placeholder="登录账号" size="large">
  40 + <template #prefix><user-outlined /></template>
  41 + </a-input>
  42 + </a-form-item>
  43 + <a-form-item name="pass" :rules="[{ required: true, message: '请输入密码' }]">
  44 + <a-input-password v-model:value="form.pass" placeholder="密码" size="large">
  45 + <template #prefix><lock-outlined /></template>
  46 + </a-input-password>
  47 + </a-form-item>
  48 + <a-form-item>
  49 + <a-button type="primary" html-type="submit" block size="large" :loading="loading">
  50 + 登录进入中台
  51 + </a-button>
  52 + </a-form-item>
  53 + </a-form>
  54 + </a-card>
  55 + </div>
28 56 </div>
29 57 </template>
30 58  
... ... @@ -35,6 +63,7 @@ import { message } from &#39;ant-design-vue&#39;
35 63 import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
36 64 import { useAuthStore } from '@/stores/auth'
37 65 import request from '@/utils/request'
  66 +import heroImage from '@/assets/hero.png'
38 67  
39 68 const router = useRouter()
40 69 const auth = useAuthStore()
... ... @@ -62,6 +91,152 @@ async function onSubmit() {
62 91 display: flex;
63 92 align-items: center;
64 93 justify-content: center;
65   - background: #f0f2f5;
  94 + padding: 24px;
  95 +}
  96 +
  97 +.login-shell {
  98 + width: min(1180px, 100%);
  99 + display: grid;
  100 + grid-template-columns: minmax(0, 1.15fr) minmax(360px, 430px);
  101 + gap: 26px;
  102 +}
  103 +
  104 +.login-visual,
  105 +.login-card {
  106 + border: 1px solid rgba(255, 255, 255, 0.62);
  107 + background: rgba(255, 255, 255, 0.76);
  108 + backdrop-filter: blur(24px);
  109 + box-shadow: 0 20px 50px rgba(126, 110, 211, 0.15);
  110 +}
  111 +
  112 +.login-visual {
  113 + position: relative;
  114 + overflow: hidden;
  115 + border-radius: 36px;
  116 + padding: 42px;
  117 +}
  118 +
  119 +.visual-badge,
  120 +.head-chip {
  121 + display: inline-flex;
  122 + align-items: center;
  123 + border-radius: 999px;
  124 + padding: 8px 14px;
  125 + background: linear-gradient(135deg, rgba(140, 124, 240, 0.14), rgba(255, 210, 232, 0.26));
  126 + color: #6f5fe2;
  127 + font-size: 12px;
  128 + font-weight: 700;
  129 + text-transform: uppercase;
  130 + letter-spacing: 0.08em;
  131 +}
  132 +
  133 +.login-visual h1,
  134 +.login-card h2 {
  135 + font-family: 'Outfit', sans-serif;
  136 + color: #2f2946;
  137 +}
  138 +
  139 +.login-visual h1 {
  140 + font-size: 46px;
  141 + line-height: 1.05;
  142 + margin: 18px 0 14px;
  143 +}
  144 +
  145 +.login-visual p,
  146 +.login-card-head p {
  147 + color: #8d88a4;
  148 + max-width: 520px;
  149 +}
  150 +
  151 +.visual-stats {
  152 + display: flex;
  153 + gap: 14px;
  154 + flex-wrap: wrap;
  155 + margin: 28px 0;
  156 +}
  157 +
  158 +.visual-stats > div {
  159 + min-width: 210px;
  160 + border-radius: 22px;
  161 + background: rgba(255, 255, 255, 0.7);
  162 + padding: 14px 16px;
  163 +}
  164 +
  165 +.visual-stats span {
  166 + display: block;
  167 + color: #9893ad;
  168 + font-size: 12px;
  169 + text-transform: uppercase;
  170 + letter-spacing: 0.08em;
  171 + margin-bottom: 6px;
  172 +}
  173 +
  174 +.visual-stats strong {
  175 + color: #342d4f;
  176 + font-family: 'Outfit', sans-serif;
  177 +}
  178 +
  179 +.illustration-wrap {
  180 + position: relative;
  181 + min-height: 280px;
  182 + display: flex;
  183 + align-items: flex-end;
  184 + justify-content: center;
  185 +}
  186 +
  187 +.illustration-wrap img {
  188 + position: relative;
  189 + z-index: 2;
  190 + width: min(100%, 360px);
  191 + filter: drop-shadow(0 26px 40px rgba(137, 120, 216, 0.22));
  192 +}
  193 +
  194 +.bubble {
  195 + position: absolute;
  196 + border-radius: 999px;
  197 +}
  198 +
  199 +.bubble-a {
  200 + width: 260px;
  201 + height: 260px;
  202 + background: radial-gradient(circle, rgba(198, 185, 255, 0.55) 0%, transparent 68%);
  203 + left: 30px;
  204 + bottom: 10px;
  205 +}
  206 +
  207 +.bubble-b {
  208 + width: 120px;
  209 + height: 120px;
  210 + background: radial-gradient(circle, rgba(255, 218, 152, 0.45) 0%, transparent 70%);
  211 + right: 60px;
  212 + top: 24px;
  213 +}
  214 +
  215 +.login-card {
  216 + border-radius: 32px;
  217 + padding: 8px;
  218 +}
  219 +
  220 +.login-card-head {
  221 + margin-bottom: 20px;
  222 +}
  223 +
  224 +.login-card-head h2 {
  225 + margin: 16px 0 8px;
  226 + font-size: 32px;
  227 +}
  228 +
  229 +@media (max-width: 980px) {
  230 + .login-shell {
  231 + grid-template-columns: 1fr;
  232 + }
  233 +
  234 + .login-visual {
  235 + padding: 28px;
  236 + }
  237 +
  238 + .login-visual h1 {
  239 + font-size: 34px;
  240 + }
66 241 }
67 242 </style>
... ...
src/views/city/CityList.vue
1 1 <template>
2 2 <div>
3   - <a-card title="城市/租户管理" :bordered="false">
  3 + <a-card title="租户管理" :bordered="false">
4 4 <template #extra>
5 5 <a-button type="primary" @click="openAdd">新增</a-button>
6 6 </template>
... ... @@ -29,11 +29,11 @@
29 29 </a-card>
30 30  
31 31 <!-- 新增/编辑弹窗 -->
32   - <a-modal v-model:open="modalVisible" :title="editingId ? '编辑' : '新增城市/租户'"
  32 + <a-modal v-model:open="modalVisible" :title="editingId ? '编辑' : '新增租户'"
33 33 @ok="handleSave" :confirmLoading="saving">
34 34 <a-form :model="form" layout="vertical">
35 35 <a-form-item label="名称">
36   - <a-input v-model:value="form.name" placeholder="如:广州市 / 某租户名" />
  36 + <a-input v-model:value="form.name" placeholder="如:华东一区 / 某租户名" />
37 37 </a-form-item>
38 38 <a-form-item label="区划码/编号(选填)">
39 39 <a-input v-model:value="form.areaCode" placeholder="行政区划码或自定义编号" />
... ...
src/views/dashboard/DashboardHome.vue 0 → 100644
  1 +<template>
  2 + <div class="dashboard-home">
  3 + <section class="hero-card">
  4 + <div class="hero-copy">
  5 + <div class="hero-pill">Soft-Neo Dashboard</div>
  6 + <h2>欢迎回到地利外卖运营工作台</h2>
  7 + <p>把租户、骑手、订单和分站管理集中在一张更轻盈的首页里,业务页面本身只保留导航与内容,让操作区域更大、更专注。</p>
  8 + <div class="hero-grid">
  9 + <div class="hero-metric">
  10 + <span>Operation Focus</span>
  11 + <strong>租户 · 骑手 · 订单</strong>
  12 + </div>
  13 + <div class="hero-metric">
  14 + <span>Visual Mood</span>
  15 + <strong>Lavender / Warm Glow</strong>
  16 + </div>
  17 + </div>
  18 + </div>
  19 + <div class="hero-visual">
  20 + <div class="orb orb-a"></div>
  21 + <div class="orb orb-b"></div>
  22 + <img :src="heroImage" alt="dashboard illustration" />
  23 + </div>
  24 + </section>
  25 +
  26 + <section class="content-grid">
  27 + <a-card title="快捷入口" :bordered="false">
  28 + <div class="quick-links">
  29 + <button v-for="item in quickLinks" :key="item.path" class="quick-link" type="button" @click="go(item.path)">
  30 + <strong>{{ item.title }}</strong>
  31 + <span>{{ item.desc }}</span>
  32 + </button>
  33 + </div>
  34 + </a-card>
  35 +
  36 + <a-card title="界面说明" :bordered="false">
  37 + <ul class="soft-notes">
  38 + <li>首页保留大视觉和信息卡片,适合做总览和快捷入口。</li>
  39 + <li>其他菜单页只保留紧凑头部和内容区,避免公共模块挤压表格空间。</li>
  40 + <li>整体主题继续沿用柔紫渐变、软阴影和圆角卡片。</li>
  41 + </ul>
  42 + </a-card>
  43 + </section>
  44 + </div>
  45 +</template>
  46 +
  47 +<script setup lang="ts">
  48 +import { useRouter } from 'vue-router'
  49 +import heroImage from '@/assets/hero.png'
  50 +
  51 +const router = useRouter()
  52 +
  53 +const quickLinks = [
  54 + { path: '/city', title: '租户管理', desc: '配置配送费、骑手等级与租户信息' },
  55 + { path: '/rider', title: '骑手管理', desc: '查看骑手、设置等级和账号状态' },
  56 + { path: '/order', title: '订单列表', desc: '集中处理配送中的订单流转' },
  57 + { path: '/substation', title: '分站管理', desc: '维护租户站点账号和权限' },
  58 +]
  59 +
  60 +function go(path: string) {
  61 + router.push(path)
  62 +}
  63 +</script>
  64 +
  65 +<style scoped>
  66 +.dashboard-home {
  67 + display: grid;
  68 + gap: 22px;
  69 +}
  70 +
  71 +.hero-card {
  72 + display: grid;
  73 + grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.9fr);
  74 + gap: 18px;
  75 + border-radius: 34px;
  76 + padding: 28px;
  77 + background:
  78 + linear-gradient(160deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.62)),
  79 + linear-gradient(135deg, rgba(198, 185, 255, 0.72), rgba(255, 217, 236, 0.78));
  80 + border: 1px solid rgba(255, 255, 255, 0.62);
  81 + box-shadow: 0 18px 44px rgba(132, 114, 212, 0.14);
  82 +}
  83 +
  84 +.hero-pill {
  85 + display: inline-flex;
  86 + padding: 8px 14px;
  87 + border-radius: 999px;
  88 + background: rgba(255, 255, 255, 0.76);
  89 + color: #6f5fe2;
  90 + font-size: 12px;
  91 + font-weight: 700;
  92 + text-transform: uppercase;
  93 + letter-spacing: 0.08em;
  94 +}
  95 +
  96 +.hero-copy h2 {
  97 + margin: 16px 0 10px;
  98 + font-size: 34px;
  99 + line-height: 1.08;
  100 + font-family: 'Outfit', sans-serif;
  101 + color: #2f2946;
  102 +}
  103 +
  104 +.hero-copy p,
  105 +.hero-metric span,
  106 +.quick-link span,
  107 +.soft-notes {
  108 + color: #8d88a4;
  109 +}
  110 +
  111 +.hero-grid {
  112 + display: flex;
  113 + gap: 14px;
  114 + flex-wrap: wrap;
  115 + margin-top: 24px;
  116 +}
  117 +
  118 +.hero-metric {
  119 + min-width: 190px;
  120 + border-radius: 22px;
  121 + background: rgba(255, 255, 255, 0.74);
  122 + padding: 14px 16px;
  123 +}
  124 +
  125 +.hero-metric span {
  126 + display: block;
  127 + font-size: 12px;
  128 + text-transform: uppercase;
  129 + letter-spacing: 0.08em;
  130 + margin-bottom: 6px;
  131 +}
  132 +
  133 +.hero-metric strong,
  134 +.quick-link strong {
  135 + color: #322c4a;
  136 + font-family: 'Outfit', sans-serif;
  137 +}
  138 +
  139 +.hero-visual {
  140 + position: relative;
  141 + display: flex;
  142 + align-items: flex-end;
  143 + justify-content: center;
  144 + min-height: 260px;
  145 +}
  146 +
  147 +.hero-visual img {
  148 + position: relative;
  149 + z-index: 2;
  150 + width: min(100%, 320px);
  151 + filter: drop-shadow(0 24px 36px rgba(135, 119, 212, 0.24));
  152 +}
  153 +
  154 +.orb {
  155 + position: absolute;
  156 + border-radius: 999px;
  157 +}
  158 +
  159 +.orb-a {
  160 + width: 220px;
  161 + height: 220px;
  162 + background: radial-gradient(circle, rgba(198, 185, 255, 0.48) 0%, transparent 70%);
  163 + top: 8px;
  164 + right: 50px;
  165 +}
  166 +
  167 +.orb-b {
  168 + width: 100px;
  169 + height: 100px;
  170 + background: radial-gradient(circle, rgba(255, 217, 136, 0.38) 0%, transparent 70%);
  171 + right: 24px;
  172 + top: 40px;
  173 +}
  174 +
  175 +.content-grid {
  176 + display: grid;
  177 + grid-template-columns: 1.2fr 0.8fr;
  178 + gap: 18px;
  179 +}
  180 +
  181 +.quick-links {
  182 + display: grid;
  183 + grid-template-columns: repeat(2, minmax(0, 1fr));
  184 + gap: 14px;
  185 +}
  186 +
  187 +.quick-link {
  188 + text-align: left;
  189 + border: 1px solid rgba(202, 193, 240, 0.34);
  190 + background: rgba(255, 255, 255, 0.78);
  191 + border-radius: 24px;
  192 + padding: 18px;
  193 + cursor: pointer;
  194 +}
  195 +
  196 +.soft-notes {
  197 + margin: 0;
  198 + padding-left: 18px;
  199 +}
  200 +
  201 +.soft-notes li + li {
  202 + margin-top: 10px;
  203 +}
  204 +
  205 +@media (max-width: 980px) {
  206 + .hero-card,
  207 + .content-grid,
  208 + .quick-links {
  209 + grid-template-columns: 1fr;
  210 + }
  211 +
  212 + .hero-copy h2 {
  213 + font-size: 28px;
  214 + }
  215 +}
  216 +</style>
... ...
src/views/merchant/StoreList.vue
... ... @@ -3,7 +3,7 @@
3 3 <a-card title="店铺管理" :bordered="false">
4 4 <template #extra>
5 5 <a-space>
6   - <a-select v-model:value="filterCityId" placeholder="选择城市" allowClear style="width:150px" @change="loadList">
  6 + <a-select v-model:value="filterCityId" placeholder="选择租户" allowClear style="width:150px" @change="loadList">
7 7 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
8 8 </a-select>
9 9 <a-input-search v-model:value="keyword" placeholder="搜索店铺名" @search="loadList" style="width:200px" />
... ... @@ -43,8 +43,8 @@
43 43 @ok="handleSave" :confirmLoading="saving" width="600px">
44 44 <a-form :model="form" layout="vertical">
45 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="选择城市">
  46 + <a-form-item label="所属租户">
  47 + <a-select v-model:value="form.cityId" placeholder="选择租户">
48 48 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
49 49 </a-select>
50 50 </a-form-item>
... ... @@ -111,7 +111,7 @@ const feeForm = reactive({ freeShipping: 0, upToSend: 0 })
111 111 const columns = [
112 112 { title: 'ID', dataIndex: 'id', width: 80 },
113 113 { title: '店铺名', dataIndex: 'name' },
114   - { title: '城市', dataIndex: 'cityId' },
  114 + { title: '租户', dataIndex: 'cityId' },
115 115 { title: '外部编号', dataIndex: 'outStoreId', ellipsis: true },
116 116 { title: '接入方', dataIndex: 'appKey', ellipsis: true },
117 117 { title: '地址', dataIndex: 'address', ellipsis: true },
... ...
src/views/open/OpenAppList.vue
... ... @@ -36,8 +36,8 @@
36 36 <a-modal v-model:open="addVisible" title="创建应用" @ok="handleCreate" :confirmLoading="saving">
37 37 <a-form :model="addForm" layout="vertical">
38 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%">
  39 + <a-form-item label="关联租户(必填)">
  40 + <a-select v-model:value="addForm.cityId" placeholder="选择租户" style="width:100%">
41 41 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
42 42 </a-select>
43 43 </a-form-item>
... ... @@ -98,7 +98,7 @@ const columns = [
98 98 { title: 'ID', dataIndex: 'id', width: 80 },
99 99 { title: '应用名称', dataIndex: 'appName' },
100 100 { title: 'AppKey', key: 'appKey' },
101   - { title: '城市/租户', dataIndex: 'cityId' },
  101 + { title: '租户', dataIndex: 'cityId' },
102 102 { title: '状态', key: 'status' },
103 103 { title: '回调地址', dataIndex: 'webhookUrl', ellipsis: true },
104 104 { title: '操作', key: 'action' },
... ... @@ -133,7 +133,7 @@ function openAdd() {
133 133 }
134 134  
135 135 async function handleCreate() {
136   - if (!addForm.cityId) { message.error('请选择城市/租户'); return }
  136 + if (!addForm.cityId) { message.error('请选择租户'); return }
137 137 saving.value = true
138 138 try {
139 139 await openApi.create(addForm)
... ...
src/views/order/OrderList.vue
... ... @@ -51,8 +51,25 @@
51 51 <!-- 指派骑手弹窗 -->
52 52 <a-modal v-model:open="designateVisible" title="指派骑手" @ok="handleDesignate" :confirmLoading="saving">
53 53 <a-form layout="vertical">
54   - <a-form-item label="骑手ID">
55   - <a-input-number v-model:value="designateRiderId" style="width:100%" placeholder="输入骑手ID" />
  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})`"
  69 + >
  70 + {{ item.userNickname || '未命名' }}(ID:{{ item.id }} / {{ item.mobile || '无手机号' }} / {{ item.isRest === 1 ? '休息' : '在线' }})
  71 + </a-select-option>
  72 + </a-select>
56 73 </a-form-item>
57 74 </a-form>
58 75 </a-modal>
... ... @@ -98,11 +115,13 @@ const filterStatus = ref&lt;number | undefined&gt;()
98 115 const filterTrans = ref<number | undefined>()
99 116 const keyword = ref('')
100 117 const designateVisible = ref(false)
  118 +const candidateLoading = ref(false)
101 119 const refundVisible = ref(false)
102 120 const rejectVisible = ref(false)
103 121 const rejectRemark = ref('')
104 122 const designateOrderId = ref(0)
105 123 const designateRiderId = ref<number | undefined>()
  124 +const designateCandidates = ref<any[]>([])
106 125 const refundRecord = ref<any>(null)
107 126 const currentRefundRecordId = ref(0)
108 127  
... ... @@ -138,14 +157,26 @@ async function loadList() {
138 157 } finally { loading.value = false }
139 158 }
140 159  
141   -function openDesignate(record: any) {
  160 +async function openDesignate(record: any) {
142 161 designateOrderId.value = record.id
143 162 designateRiderId.value = undefined
  163 + designateCandidates.value = []
  164 + candidateLoading.value = true
  165 + try {
  166 + const res: any = await riderApi.designateCandidates(record.id)
  167 + designateCandidates.value = Array.isArray(res?.data) ? res.data : []
  168 + } finally {
  169 + candidateLoading.value = false
  170 + }
144 171 designateVisible.value = true
145 172 }
146 173  
  174 +function filterCandidateOption(input: string, option: any) {
  175 + return String(option.label || '').toLowerCase().includes(input.toLowerCase())
  176 +}
  177 +
147 178 async function handleDesignate() {
148   - if (!designateRiderId.value) { message.error('请输入骑手ID'); return }
  179 + if (!designateRiderId.value) { message.error('请选择骑手'); return }
149 180 saving.value = true
150 181 try {
151 182 await riderApi.designate(designateOrderId.value, designateRiderId.value)
... ...
src/views/rider/RiderEvaluateList.vue
... ... @@ -38,7 +38,7 @@ const columns = [
38 38 { title: '骑手ID', dataIndex: 'rid' },
39 39 { title: '评分', key: 'star' },
40 40 { title: '内容', dataIndex: 'content', ellipsis: true },
41   - { title: '城市', dataIndex: 'cityId' },
  41 + { title: '租户', dataIndex: 'cityId' },
42 42 ]
43 43  
44 44 async function loadList() {
... ...
src/views/rider/RiderList.vue
... ... @@ -27,6 +27,11 @@
27 27 {{ getAccountStatus(record) === 1 ? '正常' : '禁用' }}
28 28 </a-tag>
29 29 </template>
  30 + <template v-if="column.key === 'workStatus'">
  31 + <a-tag :color="getWorkStatus(record) === 0 ? 'green' : 'orange'">
  32 + {{ getWorkStatus(record) === 0 ? '在线' : '休息' }}
  33 + </a-tag>
  34 + </template>
30 35 <template v-if="column.key === 'type'">
31 36 <a-tag>{{ record.type === 1 ? '兼职' : '全职' }}</a-tag>
32 37 </template>
... ... @@ -63,8 +68,8 @@
63 68  
64 69 <a-modal v-model:open="modalVisible" title="新增骑手" @ok="handleAdd" :confirmLoading="saving">
65 70 <a-form :model="form" layout="vertical">
66   - <a-form-item v-if="isAdmin" label="城市">
67   - <a-select v-model:value="form.cityId" placeholder="选择城市">
  71 + <a-form-item v-if="isAdmin" label="租户">
  72 + <a-select v-model:value="form.cityId" placeholder="选择租户">
68 73 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
69 74 </a-select>
70 75 </a-form-item>
... ... @@ -132,11 +137,12 @@ const columns = [
132 137 { title: 'ID', dataIndex: 'id', width: 80 },
133 138 { title: '昵称', dataIndex: 'userNickname' },
134 139 { title: '手机', dataIndex: 'mobile' },
135   - { title: '城市ID', dataIndex: 'cityId' },
  140 + { title: '租户ID', dataIndex: 'cityId' },
136 141 { title: '等级', key: 'levelName' },
137 142 { title: '类型', key: 'type' },
138 143 { title: '审核状态', key: 'userStatus' },
139 144 { title: '账号状态', key: 'accountStatus' },
  145 + { title: '骑手状态', key: 'workStatus' },
140 146 { title: '余额', dataIndex: 'balance' },
141 147 { title: '操作', key: 'action' },
142 148 ]
... ... @@ -145,6 +151,10 @@ function getAccountStatus(record: any) {
145 151 return record.status === 0 ? 0 : 1
146 152 }
147 153  
  154 +function getWorkStatus(record: any) {
  155 + return record.isRest === 1 ? 1 : 0
  156 +}
  157 +
148 158 async function loadList() {
149 159 loading.value = true
150 160 try {
... ... @@ -177,7 +187,7 @@ async function handleAdd() {
177 187 return
178 188 }
179 189 if (isAdmin.value && !form.cityId) {
180   - message.error('请选择城市')
  190 + message.error('请选择租户')
181 191 return
182 192 }
183 193  
... ...
src/views/substation/SubstationList.vue
... ... @@ -37,8 +37,8 @@
37 37 <a-modal v-model:open="modalVisible" :title="editingId ? '编辑分站' : '新增分站'"
38 38 @ok="handleSave" :confirmLoading="saving">
39 39 <a-form :model="form" layout="vertical">
40   - <a-form-item label="管理城市">
41   - <a-select v-model:value="form.cityId" placeholder="选择城市">
  40 + <a-form-item label="所属租户">
  41 + <a-select v-model:value="form.cityId" placeholder="选择租户">
42 42 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
43 43 </a-select>
44 44 </a-form-item>
... ... @@ -90,7 +90,7 @@ const columns = [
90 90 { title: '账号', dataIndex: 'userLogin' },
91 91 { title: '昵称', dataIndex: 'userNickname' },
92 92 { title: '手机', dataIndex: 'mobile' },
93   - { title: '城市ID', dataIndex: 'cityId' },
  93 + { title: '租户ID', dataIndex: 'cityId' },
94 94 { title: '状态', key: 'status' },
95 95 { title: '操作', key: 'action' },
96 96 ]
... ...