Commit fdbd231e57745f4c37fb92a5c79eeb3ea668a1ab

Authored by 杨刚
1 parent 8a06965a

init

src/api/index.ts
@@ -56,6 +56,8 @@ export const riderApi = { @@ -56,6 +56,8 @@ export const riderApi = {
56 request.post('/api/admin/rider/setEnableStatus', null, { params: { riderId, status } }), 56 request.post('/api/admin/rider/setEnableStatus', null, { params: { riderId, status } }),
57 setType: (riderId: number, type: number) => 57 setType: (riderId: number, type: number) =>
58 request.post('/api/admin/rider/setType', null, { params: { riderId, type } }), 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 designate: (orderId: number, riderId: number) => 61 designate: (orderId: number, riderId: number) =>
60 request.post('/api/admin/rider/order/designate', null, { params: { orderId, riderId } }), 62 request.post('/api/admin/rider/order/designate', null, { params: { orderId, riderId } }),
61 setTrans: (orderId: number, trans: number) => 63 setTrans: (orderId: number, trans: number) =>
@@ -90,6 +92,7 @@ export const refundApi = { @@ -90,6 +92,7 @@ export const refundApi = {
90 export const openApi = { 92 export const openApi = {
91 list: (page = 1) => request.get('/api/platform/open/list', { params: { page } }), 93 list: (page = 1) => request.get('/api/platform/open/list', { params: { page } }),
92 create: (data: any) => request.post('/api/platform/open/create', null, { params: data }), 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 resetSecret: (appId: number) => 96 resetSecret: (appId: number) =>
94 request.post('/api/platform/open/resetSecret', null, { params: { appId } }), 97 request.post('/api/platform/open/resetSecret', null, { params: { appId } }),
95 setStatus: (appId: number, status: number) => 98 setStatus: (appId: number, status: number) =>
src/layouts/MainLayout.vue
1 <template> 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 <router-view /> 101 <router-view />
59 - </a-layout-content>  
60 - </a-layout>  
61 - </a-layout> 102 + </div>
  103 + </main>
  104 + </div>
62 </template> 105 </template>
63 106
64 <script setup lang="ts"> 107 <script setup lang="ts">
65 -import { ref, watch } from 'vue' 108 +import { computed, ref, watch } from 'vue'
66 import { useRouter, useRoute } from 'vue-router' 109 import { useRouter, useRoute } from 'vue-router'
67 import { useAuthStore } from '@/stores/auth' 110 import { useAuthStore } from '@/stores/auth'
68 import { 111 import {
69 GlobalOutlined, ApartmentOutlined, ShopOutlined, 112 GlobalOutlined, ApartmentOutlined, ShopOutlined,
70 - UserOutlined, UnorderedListOutlined, ApiOutlined, DownOutlined, StarOutlined 113 + UserOutlined, UnorderedListOutlined, ApiOutlined, DownOutlined, StarOutlined,
  114 + CalendarOutlined, MenuFoldOutlined, MenuUnfoldOutlined, HomeOutlined
71 } from '@ant-design/icons-vue' 115 } from '@ant-design/icons-vue'
72 116
73 const router = useRouter() 117 const router = useRouter()
@@ -75,9 +119,31 @@ const route = useRoute() @@ -75,9 +119,31 @@ const route = useRoute()
75 const auth = useAuthStore() 119 const auth = useAuthStore()
76 const collapsed = ref(false) 120 const collapsed = ref(false)
77 const selectedKeys = ref([route.path]) 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 watch(() => route.path, (p) => { selectedKeys.value = [p] }) 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 function onMenuClick({ key }: { key: string }) { 147 function onMenuClick({ key }: { key: string }) {
82 router.push(key) 148 router.push(key)
83 } 149 }
@@ -89,15 +155,234 @@ function handleLogout() { @@ -89,15 +155,234 @@ function handleLogout() {
89 </script> 155 </script>
90 156
91 <style scoped> 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 display: flex; 180 display: flex;
98 - align-items: center;  
99 - justify-content: center; 181 + flex-direction: column;
100 overflow: hidden; 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 </style> 388 </style>
src/main.ts
@@ -4,6 +4,7 @@ import Antd from &#39;ant-design-vue&#39; @@ -4,6 +4,7 @@ import Antd from &#39;ant-design-vue&#39;
4 import 'ant-design-vue/dist/reset.css' 4 import 'ant-design-vue/dist/reset.css'
5 import App from './App.vue' 5 import App from './App.vue'
6 import router from './router' 6 import router from './router'
  7 +import './style.css'
7 8
8 const app = createApp(App) 9 const app = createApp(App)
9 app.use(createPinia()) 10 app.use(createPinia())
src/router/index.ts
@@ -13,13 +13,19 @@ const router = createRouter({ @@ -13,13 +13,19 @@ const router = createRouter({
13 { 13 {
14 path: '/', 14 path: '/',
15 component: () => import('@/layouts/MainLayout.vue'), 15 component: () => import('@/layouts/MainLayout.vue'),
16 - redirect: '/city', 16 + redirect: '/dashboard',
17 children: [ 17 children: [
18 { 18 {
  19 + path: 'dashboard',
  20 + name: 'Dashboard',
  21 + component: () => import('@/views/dashboard/DashboardHome.vue'),
  22 + meta: { title: '工作台' },
  23 + },
  24 + {
19 path: 'city', 25 path: 'city',
20 name: 'City', 26 name: 'City',
21 component: () => import('@/views/city/CityList.vue'), 27 component: () => import('@/views/city/CityList.vue'),
22 - meta: { title: '城市管理' }, 28 + meta: { title: '租户管理' },
23 }, 29 },
24 { 30 {
25 path: 'substation', 31 path: 'substation',
@@ -75,6 +81,12 @@ const router = createRouter({ @@ -75,6 +81,12 @@ const router = createRouter({
75 component: () => import('@/views/open/OpenAppList.vue'), 81 component: () => import('@/views/open/OpenAppList.vue'),
76 meta: { title: '开放平台' }, 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 { path: '/:pathMatch(.*)*', redirect: '/' }, 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 :root { 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 font-synthesis: none; 37 font-synthesis: none;
24 text-rendering: optimizeLegibility; 38 text-rendering: optimizeLegibility;
25 -webkit-font-smoothing: antialiased; 39 -webkit-font-smoothing: antialiased;
26 -moz-osx-font-smoothing: grayscale; 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 body { 54 body {
54 margin: 0; 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 position: relative; 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 <template> 1 <template>
2 <div class="login-wrap"> 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 </div> 56 </div>
29 </template> 57 </template>
30 58
@@ -35,6 +63,7 @@ import { message } from &#39;ant-design-vue&#39; @@ -35,6 +63,7 @@ import { message } from &#39;ant-design-vue&#39;
35 import { UserOutlined, LockOutlined } from '@ant-design/icons-vue' 63 import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
36 import { useAuthStore } from '@/stores/auth' 64 import { useAuthStore } from '@/stores/auth'
37 import request from '@/utils/request' 65 import request from '@/utils/request'
  66 +import heroImage from '@/assets/hero.png'
38 67
39 const router = useRouter() 68 const router = useRouter()
40 const auth = useAuthStore() 69 const auth = useAuthStore()
@@ -62,6 +91,152 @@ async function onSubmit() { @@ -62,6 +91,152 @@ async function onSubmit() {
62 display: flex; 91 display: flex;
63 align-items: center; 92 align-items: center;
64 justify-content: center; 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 </style> 242 </style>
src/views/city/CityList.vue
1 <template> 1 <template>
2 <div> 2 <div>
3 - <a-card title="城市/租户管理" :bordered="false"> 3 + <a-card title="租户管理" :bordered="false">
4 <template #extra> 4 <template #extra>
5 <a-button type="primary" @click="openAdd">新增</a-button> 5 <a-button type="primary" @click="openAdd">新增</a-button>
6 </template> 6 </template>
@@ -29,11 +29,11 @@ @@ -29,11 +29,11 @@
29 </a-card> 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 @ok="handleSave" :confirmLoading="saving"> 33 @ok="handleSave" :confirmLoading="saving">
34 <a-form :model="form" layout="vertical"> 34 <a-form :model="form" layout="vertical">
35 <a-form-item label="名称"> 35 <a-form-item label="名称">
36 - <a-input v-model:value="form.name" placeholder="如:广州市 / 某租户名" /> 36 + <a-input v-model:value="form.name" placeholder="如:华东一区 / 某租户名" />
37 </a-form-item> 37 </a-form-item>
38 <a-form-item label="区划码/编号(选填)"> 38 <a-form-item label="区划码/编号(选填)">
39 <a-input v-model:value="form.areaCode" placeholder="行政区划码或自定义编号" /> 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,7 +3,7 @@
3 <a-card title="店铺管理" :bordered="false"> 3 <a-card title="店铺管理" :bordered="false">
4 <template #extra> 4 <template #extra>
5 <a-space> 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 <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" style="width:200px" />
@@ -43,8 +43,8 @@ @@ -43,8 +43,8 @@
43 @ok="handleSave" :confirmLoading="saving" width="600px"> 43 @ok="handleSave" :confirmLoading="saving" width="600px">
44 <a-form :model="form" layout="vertical"> 44 <a-form :model="form" layout="vertical">
45 <a-form-item label="店铺名称"><a-input v-model:value="form.name" /></a-form-item> 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 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option> 48 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
49 </a-select> 49 </a-select>
50 </a-form-item> 50 </a-form-item>
@@ -111,7 +111,7 @@ const feeForm = reactive({ freeShipping: 0, upToSend: 0 }) @@ -111,7 +111,7 @@ const feeForm = reactive({ freeShipping: 0, upToSend: 0 })
111 const columns = [ 111 const columns = [
112 { title: 'ID', dataIndex: 'id', width: 80 }, 112 { title: 'ID', dataIndex: 'id', width: 80 },
113 { title: '店铺名', dataIndex: 'name' }, 113 { title: '店铺名', dataIndex: 'name' },
114 - { title: '城市', dataIndex: 'cityId' }, 114 + { title: '租户', dataIndex: 'cityId' },
115 { title: '外部编号', dataIndex: 'outStoreId', ellipsis: true }, 115 { title: '外部编号', dataIndex: 'outStoreId', ellipsis: true },
116 { title: '接入方', dataIndex: 'appKey', ellipsis: true }, 116 { title: '接入方', dataIndex: 'appKey', ellipsis: true },
117 { title: '地址', dataIndex: 'address', ellipsis: true }, 117 { title: '地址', dataIndex: 'address', ellipsis: true },
src/views/open/OpenAppList.vue
@@ -36,8 +36,8 @@ @@ -36,8 +36,8 @@
36 <a-modal v-model:open="addVisible" title="创建应用" @ok="handleCreate" :confirmLoading="saving"> 36 <a-modal v-model:open="addVisible" title="创建应用" @ok="handleCreate" :confirmLoading="saving">
37 <a-form :model="addForm" layout="vertical"> 37 <a-form :model="addForm" layout="vertical">
38 <a-form-item label="应用名称"><a-input v-model:value="addForm.appName" /></a-form-item> 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 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option> 41 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
42 </a-select> 42 </a-select>
43 </a-form-item> 43 </a-form-item>
@@ -98,7 +98,7 @@ const columns = [ @@ -98,7 +98,7 @@ const columns = [
98 { title: 'ID', dataIndex: 'id', width: 80 }, 98 { title: 'ID', dataIndex: 'id', width: 80 },
99 { title: '应用名称', dataIndex: 'appName' }, 99 { title: '应用名称', dataIndex: 'appName' },
100 { title: 'AppKey', key: 'appKey' }, 100 { title: 'AppKey', key: 'appKey' },
101 - { title: '城市/租户', dataIndex: 'cityId' }, 101 + { title: '租户', dataIndex: 'cityId' },
102 { title: '状态', key: 'status' }, 102 { title: '状态', key: 'status' },
103 { title: '回调地址', dataIndex: 'webhookUrl', ellipsis: true }, 103 { title: '回调地址', dataIndex: 'webhookUrl', ellipsis: true },
104 { title: '操作', key: 'action' }, 104 { title: '操作', key: 'action' },
@@ -133,7 +133,7 @@ function openAdd() { @@ -133,7 +133,7 @@ function openAdd() {
133 } 133 }
134 134
135 async function handleCreate() { 135 async function handleCreate() {
136 - if (!addForm.cityId) { message.error('请选择城市/租户'); return } 136 + if (!addForm.cityId) { message.error('请选择租户'); return }
137 saving.value = true 137 saving.value = true
138 try { 138 try {
139 await openApi.create(addForm) 139 await openApi.create(addForm)
src/views/order/OrderList.vue
@@ -51,8 +51,25 @@ @@ -51,8 +51,25 @@
51 <!-- 指派骑手弹窗 --> 51 <!-- 指派骑手弹窗 -->
52 <a-modal v-model:open="designateVisible" title="指派骑手" @ok="handleDesignate" :confirmLoading="saving"> 52 <a-modal v-model:open="designateVisible" title="指派骑手" @ok="handleDesignate" :confirmLoading="saving">
53 <a-form layout="vertical"> 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 </a-form-item> 73 </a-form-item>
57 </a-form> 74 </a-form>
58 </a-modal> 75 </a-modal>
@@ -98,11 +115,13 @@ const filterStatus = ref&lt;number | undefined&gt;() @@ -98,11 +115,13 @@ const filterStatus = ref&lt;number | undefined&gt;()
98 const filterTrans = ref<number | undefined>() 115 const filterTrans = ref<number | undefined>()
99 const keyword = ref('') 116 const keyword = ref('')
100 const designateVisible = ref(false) 117 const designateVisible = ref(false)
  118 +const candidateLoading = ref(false)
101 const refundVisible = ref(false) 119 const refundVisible = ref(false)
102 const rejectVisible = ref(false) 120 const rejectVisible = ref(false)
103 const rejectRemark = ref('') 121 const rejectRemark = ref('')
104 const designateOrderId = ref(0) 122 const designateOrderId = ref(0)
105 const designateRiderId = ref<number | undefined>() 123 const designateRiderId = ref<number | undefined>()
  124 +const designateCandidates = ref<any[]>([])
106 const refundRecord = ref<any>(null) 125 const refundRecord = ref<any>(null)
107 const currentRefundRecordId = ref(0) 126 const currentRefundRecordId = ref(0)
108 127
@@ -138,14 +157,26 @@ async function loadList() { @@ -138,14 +157,26 @@ async function loadList() {
138 } finally { loading.value = false } 157 } finally { loading.value = false }
139 } 158 }
140 159
141 -function openDesignate(record: any) { 160 +async function openDesignate(record: any) {
142 designateOrderId.value = record.id 161 designateOrderId.value = record.id
143 designateRiderId.value = undefined 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 designateVisible.value = true 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 async function handleDesignate() { 178 async function handleDesignate() {
148 - if (!designateRiderId.value) { message.error('请输入骑手ID'); return } 179 + if (!designateRiderId.value) { message.error('请选择骑手'); return }
149 saving.value = true 180 saving.value = true
150 try { 181 try {
151 await riderApi.designate(designateOrderId.value, designateRiderId.value) 182 await riderApi.designate(designateOrderId.value, designateRiderId.value)
src/views/rider/RiderEvaluateList.vue
@@ -38,7 +38,7 @@ const columns = [ @@ -38,7 +38,7 @@ const columns = [
38 { title: '骑手ID', dataIndex: 'rid' }, 38 { title: '骑手ID', dataIndex: 'rid' },
39 { title: '评分', key: 'star' }, 39 { title: '评分', key: 'star' },
40 { title: '内容', dataIndex: 'content', ellipsis: true }, 40 { title: '内容', dataIndex: 'content', ellipsis: true },
41 - { title: '城市', dataIndex: 'cityId' }, 41 + { title: '租户', dataIndex: 'cityId' },
42 ] 42 ]
43 43
44 async function loadList() { 44 async function loadList() {
src/views/rider/RiderList.vue
@@ -27,6 +27,11 @@ @@ -27,6 +27,11 @@
27 {{ getAccountStatus(record) === 1 ? '正常' : '禁用' }} 27 {{ getAccountStatus(record) === 1 ? '正常' : '禁用' }}
28 </a-tag> 28 </a-tag>
29 </template> 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 <template v-if="column.key === 'type'"> 35 <template v-if="column.key === 'type'">
31 <a-tag>{{ record.type === 1 ? '兼职' : '全职' }}</a-tag> 36 <a-tag>{{ record.type === 1 ? '兼职' : '全职' }}</a-tag>
32 </template> 37 </template>
@@ -63,8 +68,8 @@ @@ -63,8 +68,8 @@
63 68
64 <a-modal v-model:open="modalVisible" title="新增骑手" @ok="handleAdd" :confirmLoading="saving"> 69 <a-modal v-model:open="modalVisible" title="新增骑手" @ok="handleAdd" :confirmLoading="saving">
65 <a-form :model="form" layout="vertical"> 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 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option> 73 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
69 </a-select> 74 </a-select>
70 </a-form-item> 75 </a-form-item>
@@ -132,11 +137,12 @@ const columns = [ @@ -132,11 +137,12 @@ const columns = [
132 { title: 'ID', dataIndex: 'id', width: 80 }, 137 { title: 'ID', dataIndex: 'id', width: 80 },
133 { title: '昵称', dataIndex: 'userNickname' }, 138 { title: '昵称', dataIndex: 'userNickname' },
134 { title: '手机', dataIndex: 'mobile' }, 139 { title: '手机', dataIndex: 'mobile' },
135 - { title: '城市ID', dataIndex: 'cityId' }, 140 + { title: '租户ID', dataIndex: 'cityId' },
136 { title: '等级', key: 'levelName' }, 141 { title: '等级', key: 'levelName' },
137 { title: '类型', key: 'type' }, 142 { title: '类型', key: 'type' },
138 { title: '审核状态', key: 'userStatus' }, 143 { title: '审核状态', key: 'userStatus' },
139 { title: '账号状态', key: 'accountStatus' }, 144 { title: '账号状态', key: 'accountStatus' },
  145 + { title: '骑手状态', key: 'workStatus' },
140 { title: '余额', dataIndex: 'balance' }, 146 { title: '余额', dataIndex: 'balance' },
141 { title: '操作', key: 'action' }, 147 { title: '操作', key: 'action' },
142 ] 148 ]
@@ -145,6 +151,10 @@ function getAccountStatus(record: any) { @@ -145,6 +151,10 @@ function getAccountStatus(record: any) {
145 return record.status === 0 ? 0 : 1 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 async function loadList() { 158 async function loadList() {
149 loading.value = true 159 loading.value = true
150 try { 160 try {
@@ -177,7 +187,7 @@ async function handleAdd() { @@ -177,7 +187,7 @@ async function handleAdd() {
177 return 187 return
178 } 188 }
179 if (isAdmin.value && !form.cityId) { 189 if (isAdmin.value && !form.cityId) {
180 - message.error('请选择城市') 190 + message.error('请选择租户')
181 return 191 return
182 } 192 }
183 193
src/views/substation/SubstationList.vue
@@ -37,8 +37,8 @@ @@ -37,8 +37,8 @@
37 <a-modal v-model:open="modalVisible" :title="editingId ? '编辑分站' : '新增分站'" 37 <a-modal v-model:open="modalVisible" :title="editingId ? '编辑分站' : '新增分站'"
38 @ok="handleSave" :confirmLoading="saving"> 38 @ok="handleSave" :confirmLoading="saving">
39 <a-form :model="form" layout="vertical"> 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 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option> 42 <a-select-option v-for="c in cityList" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
43 </a-select> 43 </a-select>
44 </a-form-item> 44 </a-form-item>
@@ -90,7 +90,7 @@ const columns = [ @@ -90,7 +90,7 @@ const columns = [
90 { title: '账号', dataIndex: 'userLogin' }, 90 { title: '账号', dataIndex: 'userLogin' },
91 { title: '昵称', dataIndex: 'userNickname' }, 91 { title: '昵称', dataIndex: 'userNickname' },
92 { title: '手机', dataIndex: 'mobile' }, 92 { title: '手机', dataIndex: 'mobile' },
93 - { title: '城市ID', dataIndex: 'cityId' }, 93 + { title: '租户ID', dataIndex: 'cityId' },
94 { title: '状态', key: 'status' }, 94 { title: '状态', key: 'status' },
95 { title: '操作', key: 'action' }, 95 { title: '操作', key: 'action' },
96 ] 96 ]