| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- <template>
- <div class="dispatch-container">
- <!-- Top Filter Bar -->
- <div class="top-filter-bar glass-panel-sm">
- <div class="filter-left">
- <el-radio-group v-model="filters.orderType" size="default" fill="#2d8cf0">
- <el-radio-button label="all">全部</el-radio-button>
- <el-radio-button v-for="item in serviceOnOrderList" :key="item.id" :label="item.id">{{ item.name
- }}</el-radio-button>
- </el-radio-group>
- </div>
- <div class="filter-right">
- <el-cascader v-model="regionValue" :options="areaOptions" placeholder="省市区域" clearable
- style="width: 170px; margin-right: 10px" @change="handleAreaChange" />
- <el-select v-model="filters.station" placeholder="站点" clearable style="width: 170px">
- <el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- </div>
- </div>
- <div class="main-content">
- <!-- Left: Real Gaode Map Area -->
- <div class="map-wrapper">
- <template v-if="checkPermi(['order:dispatch:map'])">
- <div id="amap-container" class="map-view"></div>
- <!-- Bottom Left: Map Controls & Stats -->
- <div class="map-controls-panel">
- <div class="control-group">
- <div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
- <div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
- @click="setMapFilter('merchants')">商家({{ merchantList.length }})</div>
- <div class="c-btn green" :class="{ active: activeMapFilter === 'fulfillers' }"
- @click="setMapFilter('fulfillers')">履约者({{ ridersList.length }})</div>
- <div class="c-btn blue" :class="{ active: activeMapFilter === 'orders' }" @click="setMapFilter('orders')">
- 订单({{ ordersList.length }})</div>
- <div class="c-btn gray">灰色表示离线</div>
- </div>
- </div>
- </template>
- <div v-else class="no-auth-map">
- <div class="no-auth-content">
- <div class="lock-icon">
- <!-- Inline SVG for Lock Icon -->
- <svg viewBox="0 0 1024 1024" width="64" height="64">
- <path d="M512 64a256 256 0 0 0-256 256v128h-64a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V512a64 64 0 0 0-64-64h-64V320a256 256 0 0 0-256-256z m160 384H352V320a160 160 0 1 1 320 0v128z" fill="#E4E7ED"/>
- </svg>
- </div>
- <h3 class="no-auth-title">暂无地图权限</h3>
- <p class="no-auth-desc">当前角色尚未分配地图查看权限,无法展示实时调度数据</p>
- </div>
- </div>
- </div>
- <!-- Right: Dispatch Control Panel -->
- <div class="right-panel">
- <OrderListPanel v-model="currentOrderTab" :orders="filteredOrders" :stats="orderStats" @focus="focusMapPoint"
- @dispatch="openDispatchDialog" />
- <RiderListPanel v-model="currentRiderTab" :riders="filteredRiders" :stats="riderStats" @focus="focusMapPoint"
- @view-orders="handleViewRiderOrders" />
- </div>
- </div>
- <RiderOrdersDialog v-model="riderOrdersVisible" :riderInfo="currentRiderInfo" :orders="currentRiderOrders" />
- <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
- :service-list="serviceOnOrderList" @submit="handleDispatchSubmit" />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, computed, reactive, onMounted, watch, getCurrentInstance, ComponentInternalInstance } from 'vue';
- import { checkPermi } from "@/utils/permission";
- import { ElMessage } from 'element-plus';
- import { listAllService } from '@/api/service/list/index'
- import { listAreaStation } from '@/api/system/areaStation'
- import { listStoreOnDispatch } from '@/api/system/store/index'
- import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
- import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
- import { getFulfillerGps } from '@/api/fulfiller/fulfiller/index'
- import OrderListPanel from './components/OrderListPanel.vue';
- import RiderListPanel from './components/RiderListPanel.vue';
- import RiderOrdersDialog from './components/RiderOrdersDialog.vue';
- import DispatchDialog from './components/DispatchDialog.vue';
- // Mock Data
- import dispatchMockData from '@/mock/dispatch.json';
- import riderOrdersMockData from '@/mock/RiderOrdersDialog.json';
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- // --- Data & State ---
- const filters = reactive({
- orderType: 'all',
- station: undefined
- });
- const currentOrderTab = ref('PendingDispatch');
- const currentRiderTab = ref('All');
- const activeMapFilter = ref('all');
- const ordersList = ref([]);
- const ridersList = ref([]);
- const merchantList = ref([]);
- const orderStats = computed(() => ({
- pendingDispatch: ordersList.value.filter(o => o.status === 0).length,
- pendingAccept: ordersList.value.filter(o => o.status === 1).length,
- processing: ordersList.value.filter(o => [2, 3].includes(o.status)).length
- }));
- const riderStats = computed(() => ({
- all: ridersList.value.length,
- working: ridersList.value.filter(r => r.uiStatus === 'busy').length,
- resting: ridersList.value.filter(r => r.uiStatus === 'offline').length,
- disabled: ridersList.value.filter(r => r.uiStatus === 'disabled').length
- }));
- const serviceOnOrderList = ref([])
- const getServiceList = () => {
- listAllService().then(res => {
- serviceOnOrderList.value = res?.data || []
- }).catch(() => {
- serviceOnOrderList.value = []
- })
- }
- const getOrdersList = () => {
- if (!filters.station) return;
- listSubOrderOnDispatch({
- service: filters.orderType !== 'all' ? filters.orderType : undefined,
- site: filters.station
- }).then(res => {
- const list = (res?.data || []).map(item => {
- const typeName = getServiceName(item.service);
- const isTransport = item.mode === 1 || item.mode === '1';
- const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
- // 模拟经纬度
- return {
- ...item,
- typeCode,
- lng: item.lng || (116.4 + Math.random() * 0.1),
- lat: item.lat || (39.9 + Math.random() * 0.05)
- };
- });
- ordersList.value = list;
- })
- }
- const getServiceName = (serviceId) => {
- const item = serviceOnOrderList.value.find((i) => i.id === serviceId);
- return item ? item.name : '未知服务';
- };
- const getRidersList = async () => {
- if (!filters.station) return;
- try {
- const res = await listFulfillerOnDispatch({
- service: filters.orderType !== 'all' ? filters.orderType : undefined,
- site: filters.station
- });
- const baseList = (res.data || []).map(r => ({
- ...r,
- uiStatus: r.status === 'busy' ? 'busy' : r.status === 'resting' ? 'offline' : 'disabled',
- maskPhone: r.phone ? r.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
- categories: (r.serviceTypes || '').split(',').map(id => {
- const s = serviceOnOrderList.value.find(item => String(item.id) === String(id));
- return s ? s.name.substring(0, 2) : '服务';
- }),
- nextOrderTime: r.nextOrderTime || '14:30',
- // GPS 坐标初始为 null,等待并发请求后覆盖
- lng: null,
- lat: null
- }));
- // 并发请求所有骑手的实时 GPS 坐标
- const gpsResults = await Promise.allSettled(
- baseList.map(r => getFulfillerGps(r.id))
- );
- ridersList.value = baseList.map((r, idx) => {
- const result = gpsResults[idx];
- // 只有当 data 不为空且含有经纬度数据时才赋値
- if (result.status === 'fulfilled' && result.value?.data) {
- const gps = result.value.data;
- if (gps.longitude && gps.latitude) {
- return { ...r, lng: gps.longitude, lat: gps.latitude };
- }
- }
- return r; // 无数据保持 lng/lat 为 null,不在地图上显示
- });
- refreshMarkers();
- } catch {
- ridersList.value = [];
- refreshMarkers();
- }
- }
- const getMerchantList = () => {
- if (!filters.station) {
- merchantList.value = [];
- refreshMarkers();
- return;
- }
- listStoreOnDispatch({ site: filters.station }).then(res => {
- // 保留全量门店数据(用于统计数字),有坐标的赋 lng/lat,无坐标则不在地图打点
- merchantList.value = (res?.data || []).map(item => ({
- ...item,
- lng: item.longitude || null,
- lat: item.latitude || null
- }));
- refreshMarkers();
- }).catch(() => {
- merchantList.value = [];
- refreshMarkers();
- });
- }
- watch([() => filters.orderType, () => filters.station], () => {
- if (!filters.station) {
- ordersList.value = [];
- ridersList.value = [];
- merchantList.value = [];
- refreshMarkers();
- return;
- }
- getOrdersList();
- getRidersList();
- getMerchantList();
- });
- const areaStationList = ref([])
- const areaOptions = ref([])
- const siteOptions = ref([])
- const regionValue = ref([])
- const buildTree = (data, parentId) => {
- return (data || [])
- .filter(item => String(item.parentId) === String(parentId))
- .map(item => ({
- value: item.id,
- label: item.name,
- children: buildTree(data, item.id)
- }))
- }
- const handleAreaChange = (value) => {
- filters.station = undefined
- if (value && value.length > 0) {
- const areaId = value[value.length - 1]
- siteOptions.value = areaStationList.value
- .filter(item => Number(item.type) === 2 && String(item.parentId) === String(areaId))
- .map(item => ({ value: item.id, label: item.name }))
- } else {
- siteOptions.value = []
- }
- }
- const getAreaStationList = async () => {
- try {
- const res = await listAreaStation()
- const data = res?.data || res
- areaStationList.value = Array.isArray(data) ? data : []
- const areaData = areaStationList.value.filter(item => Number(item.type) === 0 || Number(item.type) === 1)
- areaOptions.value = buildTree(areaData, 0)
- siteOptions.value = []
- const allStations = areaStationList.value.filter(item => Number(item.type) === 2)
- if (allStations.length > 0) {
- const randomStation = allStations[Math.floor(Math.random() * allStations.length)]
- const areaId = randomStation.parentId
- const path = []
- let currentId = areaId
- while (currentId && String(currentId) !== '0') {
- path.unshift(currentId)
- const currentArea = areaStationList.value.find(item => String(item.id) === String(currentId))
- if (currentArea) {
- currentId = currentArea.parentId
- } else {
- break
- }
- }
- regionValue.value = path
- siteOptions.value = areaStationList.value
- .filter(item => Number(item.type) === 2 && String(item.parentId) === String(areaId))
- .map(item => ({ value: item.id, label: item.name }))
- filters.station = randomStation.id
- }
- } catch {
- areaStationList.value = []
- areaOptions.value = []
- siteOptions.value = []
- }
- }
- // Rider Orders State
- const riderOrdersVisible = ref(false);
- const currentRiderInfo = ref(null);
- const currentRiderOrders = ref([]);
- const handleViewRiderOrders = (rider) => {
- currentRiderInfo.value = {
- name: rider.name,
- maskPhone: rider.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
- station: rider.station + ' - 东城片区',
- workType: '全职专送',
- categories: ['宠物接送', '上门喂遛', '洗护套餐']
- };
- currentRiderOrders.value = riderOrdersMockData.orders;
- riderOrdersVisible.value = true;
- };
- // --- Map Logic ---
- let map = null;
- const amapKey = 'a30e76f457c14b6570925522be37565d';
- const securityJsCode = '531ae14ec1dff87e552e1ea51e848582';
- const loadAMapScript = () => {
- // 设置安全密钥
- window._AMapSecurityConfig = {
- securityJsCode: securityJsCode,
- };
- return new Promise((resolve, reject) => {
- if (window.AMap) {
- resolve(window.AMap);
- return;
- }
- const script = document.createElement('script');
- script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}`;
- script.onload = () => resolve(window.AMap);
- script.onerror = reject;
- document.head.appendChild(script);
- });
- };
- const initMap = () => {
- if (!window.AMap) return;
- map = new AMap.Map('amap-container', {
- zoom: 14,
- center: [116.4551, 39.9255], // Chaoyang center
- mapStyle: 'amap://styles/normal' // 可以更换其他样式
- });
- refreshMarkers();
- };
- const refreshMarkers = () => {
- if (!map) return;
- map.clearMap(); // 清除所有标记
- const filter = activeMapFilter.value;
- // 1. Merchants (商家) - 仅对有经纬度的门店打点
- if (filter === 'all' || filter === 'merchants') {
- merchantList.value.filter(m => m.lng && m.lat).forEach((m) => {
- const iconImg = m.icon || 'https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png';
- const content = `
- <div style="position:relative; width: 220px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.2));">
- <div style="background:#fff; border-radius:8px; padding: 12px; display: flex; align-items: center; gap: 10px; position: relative;">
- <!-- Icon Ring -->
- <div style="width: 48px; height: 48px; border-radius: 50%; border: 4px solid #F56C6C; padding: 2px; flex-shrink: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center;">
- <img src="${iconImg}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
- </div>
- <!-- Info -->
- <div style="flex: 1; overflow: hidden;">
- <div style="font-weight: bold; font-size: 14px; color: #333; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${m.name}</div>
- <div style="font-size: 12px; color: #666;">今日 <span style="color: #F56C6C; font-weight: bold; font-size: 14px;">0</span> 单</div>
- </div>
- </div>
- <!-- Triangle -->
- <div style="width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 10px solid #fff; margin: 0 auto; margin-top: -1px;"></div>
- </div>
- `;
- const marker = new AMap.Marker({
- position: [m.lng, m.lat],
- content: content,
- offset: new AMap.Pixel(-110, -85)
- });
- map.add(marker);
- });
- }
- // 2. Fulfiller (履约者) - 仅对有真实 GPS 坐标的骑手打点
- if (filter === 'all' || filter === 'fulfillers') {
- ridersList.value.filter(r => r.lng && r.lat).forEach((r) => {
- const borderColor = r.uiStatus === 'busy' ? '#67C23A' : r.uiStatus === 'offline' ? '#909399' : '#F56C6C';
- const avatar = r.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
- const content = `
- <div style="position:relative; width: 200px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.2));">
- <div style="background:#fff; border-radius:8px; padding: 10px; display: flex; align-items: center; gap: 10px; position: relative;">
- <!-- Avatar Ring -->
- <div style="width: 44px; height: 44px; border-radius: 50%; border: 3px solid ${borderColor}; padding: 1px; flex-shrink: 0; box-sizing: border-box;">
- <img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
- </div>
- <!-- Info -->
- <div style="flex: 1; overflow: hidden;">
- <div style="font-weight: bold; font-size: 13px; color: #333; margin-bottom: 2px;">[履约者]${r.name}</div>
- <div style="font-size: 12px; color: #999;">${r.maskPhone}</div>
- </div>
- </div>
- <!-- Triangle -->
- <div style="width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid #fff; margin: 0 auto; margin-top: -1px;"></div>
- </div>
- `;
- const marker = new AMap.Marker({
- position: [r.lng, r.lat],
- content: content,
- offset: new AMap.Pixel(-100, -75)
- });
- map.add(marker);
- });
- }
- // 3. Orders (订单)
- if (filter === 'all' || filter === 'orders') {
- filteredOrders.value.forEach((o) => {
- // 默认点标记 + 文本标注
- const labelContent = `<div style="border:1px solid #409EFF;background:#fff;color:#409EFF;padding:2px 6px;border-radius:4px;font-size:12px;">${getServiceName(o.service)}</div>`;
- const marker = new AMap.Marker({
- position: [o.lng, o.lat],
- label: {
- content: labelContent,
- direction: 'top',
- offset: new AMap.Pixel(0, -5)
- }
- });
- map.add(marker);
- });
- }
- };
- const setMapFilter = (val) => {
- activeMapFilter.value = val;
- };
- const focusMapPoint = (lng, lat) => {
- if (!map) return;
- map.setCenter([lng, lat]);
- map.setZoom(16);
- };
- watch(activeMapFilter, () => {
- refreshMarkers();
- });
- watch([currentOrderTab, currentRiderTab], () => {
- refreshMarkers();
- });
- onMounted(async () => {
- getServiceList()
- await getAreaStationList()
- if (checkPermi(['order:dispatch:map'])) {
- loadAMapScript()
- .then(() => {
- initMap();
- })
- .catch((err) => {
- console.error('Map loading failed', err);
- ElMessage.error('地图加载失败,请检查网络');
- });
- }
- });
- // Dispatch Dialog State
- const dispatchDialogVisible = ref(false);
- const currentDispatchOrder = ref(null);
- const currentRider = ref(null);
- const openDispatchDialog = (order) => {
- const typeName = getServiceName(order?.service);
- const isTransport = order?.mode === 1 || order?.mode === '1';
- const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
- const t = order?.subOrderType ?? order?.type;
- const transportType =
- t === 0 || t === '0' || t === 1 || t === '1'
- ? 'round'
- : t === 2 || t === '2'
- ? 'pick'
- : t === 3 || t === '3'
- ? 'drop'
- : order?.splitType || order?.transportType;
- const toAddress = order?.toAddress || '';
- const pickAddr = isTransport ? toAddress : '';
- const dropAddr = isTransport ? toAddress : '';
- const address = isTransport ? '' : toAddress;
- const orderObj = {
- id: order.id,
- typeCode,
- transportType,
- time: order.serviceTime || order.appointTime || order.createTime,
- status: order.status,
- address,
- pickAddr,
- dropAddr,
- service: order.service,
- riderId: order.riderId || order.fulfiller || null,
- riderGender: order.fulfillerGender
- };
- currentDispatchOrder.value = orderObj;
- dispatchDialogVisible.value = true;
- };
- const handleDispatchSubmit = async (data) => {
- if (!currentDispatchOrder.value) return;
- try {
- const priceFen = Math.round(Number(data.fee || 0) * 100);
- await dispatchSubOrder({
- orderId: currentDispatchOrder.value.id,
- fulfiller: data.riderId,
- price: priceFen
- });
- ElMessage.success('派单成功');
- dispatchDialogVisible.value = false;
- getOrdersList(); // 重新加载订单列表
- } catch (error) {
- // 错误由请求拦截器处理
- }
- };
- const filteredOrders = computed(() => {
- let result = ordersList.value;
- const statusMap = {
- 'PendingDispatch': [0],
- 'PendingAccept': [1],
- 'Processing': [2, 3]
- };
- const targetStatus = statusMap[currentOrderTab.value];
- if (targetStatus) {
- result = result.filter((o) => targetStatus.includes(o.status));
- }
- return result;
- });
- const filteredRiders = computed(() => {
- if (currentRiderTab.value === 'All') return ridersList.value;
- const map = {
- 'Working': ['busy'],
- 'Resting': ['resting'],
- 'Disabled': ['disabled']
- };
- const allowed = map[currentRiderTab.value] || [];
- return ridersList.value.filter((r) => allowed.includes(r.status));
- });
- </script>
- <style scoped>
- .dispatch-container {
- height: calc(100vh - 84px);
- display: flex;
- flex-direction: column;
- background-color: #f0f2f5;
- overflow: hidden;
- }
- /* 1. Top Bar */
- .top-filter-bar {
- height: 56px;
- background: #fff;
- border-bottom: 1px solid #e4e7ed;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 24px;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02);
- z-index: 20;
- }
- /* 2. Main Content */
- .main-content {
- flex: 1;
- display: flex;
- position: relative;
- overflow: hidden;
- }
- /* Left Map Area */
- .map-wrapper {
- flex: 1;
- position: relative;
- background: #fcf9f2;
- overflow: hidden;
- }
- .map-view {
- width: 100%;
- height: 100%;
- }
- /* Map Controls */
- .map-controls-panel {
- position: absolute;
- bottom: 24px;
- left: 24px;
- display: flex;
- align-items: center;
- gap: 16px;
- z-index: 10;
- }
- .control-group {
- background: #fff;
- border-radius: 8px;
- padding: 6px;
- display: flex;
- gap: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- .c-btn {
- padding: 6px 16px;
- border-radius: 6px;
- font-size: 13px;
- cursor: pointer;
- background: #f5f7fa;
- color: #606266;
- font-weight: 500;
- transition: all 0.2s;
- }
- .c-btn.active {
- background: #2d8cf0;
- color: #fff;
- }
- .c-btn.red {
- background: #fef0f0;
- color: #f56c6c;
- border: 1px solid #fde2e2;
- }
- .c-btn.green {
- background: #f0f9eb;
- color: #67c23a;
- border: 1px solid #e1f3d8;
- }
- .c-btn.gray {
- background: #f0f2f5;
- color: #909399;
- border: 1px solid #dcdfe6;
- cursor: default;
- }
- .c-btn.blue {
- background: #ecf5ff;
- color: #409eff;
- border: 1px solid #d9ecff;
- }
- .no-auth-map {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #fcfcfd;
- background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
- background-size: 20px 20px;
- }
- .no-auth-content {
- text-align: center;
- padding: 40px;
- background: rgba(255, 255, 255, 0.8);
- backdrop-filter: blur(8px);
- border-radius: 16px;
- border: 1px solid rgba(234, 236, 239, 0.8);
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.03);
- max-width: 320px;
- }
- .lock-icon {
- margin-bottom: 24px;
- display: flex;
- justify-content: center;
- opacity: 0.8;
- }
- .no-auth-title {
- margin: 0 0 12px 0;
- color: #303133;
- font-size: 20px;
- font-weight: 600;
- }
- .no-auth-desc {
- margin: 0 0 20px 0;
- color: #909399;
- font-size: 14px;
- line-height: 1.6;
- }
- .no-auth-tip {
- display: inline-block;
- padding: 4px 12px;
- background: #f4f4f5;
- color: #a8abb2;
- font-size: 12px;
- border-radius: 4px;
- font-family: monospace;
- }
- /* Right Panel */
- .right-panel {
- width: 440px;
- background: #fff;
- border-left: 1px solid #e4e7ed;
- display: flex;
- flex-direction: column;
- box-shadow: -4px 0 16px rgba(0, 0, 0, 0.05);
- z-index: 30;
- }
- </style>
|