├── src ├── page │ ├── info │ │ ├── highwayXueye │ │ │ ├── locations.json │ │ │ └── HighwayXueye.vue │ │ ├── carPlate │ │ │ └── CarPlate.vue │ │ ├── car │ │ │ └── CarDepartment.vue │ │ └── motorHighway │ │ │ └── MotorHighway.vue │ ├── Logout.vue │ ├── pointer │ │ ├── DrivingPolicy.ts │ │ ├── Pointer.ts │ │ ├── components │ │ │ ├── PointerDetailPanel.vue │ │ │ └── PointerListPanel.vue │ │ └── PointerViewer.vue │ ├── route │ │ ├── DrivingPolicy.ts │ │ ├── Route.ts │ │ └── components │ │ │ ├── RouteDetailPanel.vue │ │ │ └── RouteLineListPanel.vue │ ├── other │ │ ├── map │ │ │ ├── dataPoint.json │ │ │ ├── dataLine.json │ │ │ ├── province.json │ │ │ └── MapLoca.vue │ │ └── About.vue │ ├── index │ │ ├── Index.vue │ │ └── LocaIndex.vue │ ├── SearchPanel.vue │ ├── MyMapLib.ts │ ├── Login.vue │ ├── tool │ │ └── circle │ │ │ ├── components │ │ │ └── CirclePanel.vue │ │ │ └── ToolCircle.vue │ ├── Register.vue │ └── debug │ │ └── debug.vue ├── scss │ ├── _plugin.scss │ ├── _overwrite-amap.scss │ ├── _gutter.scss │ ├── _overwrite-el-ui.scss │ ├── _reset.css │ ├── _variables.scss │ ├── _utility.scss │ ├── _markdown.scss │ ├── main.scss │ ├── _marker.scss │ └── animate.css ├── assets │ └── logo.png ├── api │ ├── ServerResponse.ts │ ├── fileApi.ts │ ├── routeApi.ts │ ├── pointerApi.ts │ ├── userApi.ts │ └── request.ts ├── layout │ ├── Container.vue │ ├── FooterPagination.vue │ ├── FloatingButton.vue │ ├── Logo.vue │ ├── Toolbar.vue │ ├── Layout.vue │ ├── Copyright.vue │ ├── Aside.vue │ ├── DetailPanel.vue │ └── Navbar.vue ├── main.ts ├── mapConfig.ts ├── store.ts ├── App.vue ├── lib │ └── colors.ts ├── utility.ts └── router.ts ├── public └── favicon.png ├── deploy.sh ├── tsconfig.node.json ├── CHANGELOG.md ├── .gitignore ├── tsconfig.json ├── index.html ├── vite.config.ts ├── package.json └── README.md /src/page/info/highwayXueye/locations.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/scss/_plugin.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "utility"; -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleBing/map/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KyleBing/map/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/page/Logout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /src/api/ServerResponse.ts: -------------------------------------------------------------------------------- 1 | interface ServerResponse{ 2 | message: string, 3 | success: boolean, 4 | data: any 5 | } 6 | 7 | export { 8 | type ServerResponse 9 | } 10 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd /usr/share/nginx/html && 3 | rm -Rf tools/map/* && 4 | mv map-* tools/map/ && 5 | cd tools/map && 6 | unzip map-* && 7 | rm -f map-* 8 | echo 'Map deploy finished.' 9 | -------------------------------------------------------------------------------- /src/scss/_overwrite-amap.scss: -------------------------------------------------------------------------------- 1 | // 隐藏地图中的高德地图 LOGG 2 | .amap-copyright, .amap-logo{ 3 | display: none !important; 4 | } 5 | 6 | .amap-control.amap-scalecontrol{ 7 | bottom: 10px !important; 8 | } -------------------------------------------------------------------------------- /src/api/fileApi.ts: -------------------------------------------------------------------------------- 1 | import {request} from "./request"; 2 | 3 | function getUploadToken(params) {return request('get', params, null,false, '/image-qiniu/')} 4 | export { 5 | getUploadToken 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts", "src/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ## `2025-06-17` 4 | - 添加 gpx 导出到 svg 5 | 6 | ## v1.0 `2024-07-27` 7 | - 完成 ts 迁移 8 | - 编辑对应页面时,标点可拖拽改变位置 9 | 10 | ## `2024-07-22` 11 | - 改成 vite+ts+vue3 版本 12 | 13 | ### v0.23 `2023-07-03` 14 | - 添加注册页面 15 | - 去除主页 loca 动画 16 | 17 | ### `2023-06-27` 18 | - 添加市的区县信息显示 19 | - 添加路径上的天气信息 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | 26 | archive 27 | -------------------------------------------------------------------------------- /src/api/routeApi.ts: -------------------------------------------------------------------------------- 1 | import {request} from './request' 2 | export default { 3 | add( requestData){return request('post', null, requestData, false, 'map-route/add')}, 4 | list(requestData){return request('post', null, requestData, false, 'map-route/list')}, 5 | modify(requestData){return request('put', null, requestData, false, 'map-route/modify')}, 6 | delete(requestData){return request('delete', null, requestData, false, 'map-route/delete')}, 7 | detail(params){return request('get', params, null, false, 'map-route/detail')}, 8 | } 9 | -------------------------------------------------------------------------------- /src/api/pointerApi.ts: -------------------------------------------------------------------------------- 1 | import {request} from './request' 2 | export default { 3 | add( requestData){return request('post', null, requestData, false, 'map-pointer/add')}, 4 | list(requestData){return request('post', null, requestData, false,'map-pointer/list')}, 5 | modify(requestData){return request('put', null, requestData, false, 'map-pointer/modify')}, 6 | delete(requestData){return request('delete', null, requestData, false, 'map-pointer/delete')}, 7 | detail(params){return request('get', params, null, false, 'map-pointer/detail')}, 8 | } 9 | -------------------------------------------------------------------------------- /src/layout/Container.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/scss/_gutter.scss: -------------------------------------------------------------------------------- 1 | $timer: 10px; 2 | // common 3 | @for $item from 0 through 7 { 4 | .mt-#{$item}{margin-top: $timer * $item !important;} 5 | .mb-#{$item}{margin-bottom: $timer * $item !important;} 6 | .ml-#{$item}{margin-left: $timer * $item !important;} 7 | .mr-#{$item}{margin-right: $timer * $item !important;} 8 | .m-#{$item}{margin: $timer * $item !important;} 9 | 10 | .pt-#{$item}{padding-top: $timer * $item !important;} 11 | .pb-#{$item}{padding-bottom: $timer * $item !important;} 12 | .pl-#{$item}{padding-left: $timer * $item !important;} 13 | .pr-#{$item}{padding-right: $timer * $item !important;} 14 | .p-#{$item}{padding: $timer * $item !important;} 15 | } 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // main.ts 2 | import { createApp } from 'vue' 3 | 4 | // APP 5 | import App from './App.vue' 6 | const app = createApp(App) 7 | 8 | // ELEMENT-UI 9 | import ElementPlus from 'element-plus' 10 | import 'element-plus/dist/index.css' 11 | import zhCn from 'element-plus/dist/locale/zh-cn.mjs' 12 | 13 | app.use(ElementPlus, { 14 | locale: zhCn 15 | }) 16 | 17 | // ELEMENT-UI-ICONS 18 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 19 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 20 | app.component(key, component) 21 | } 22 | 23 | // PINIA 24 | import {createPinia} from "pinia" 25 | const pinia = createPinia() 26 | app.use(pinia) 27 | 28 | 29 | // ROUTER 30 | import {router} from "./router" 31 | app.use(router) 32 | 33 | app.mount('#app') 34 | -------------------------------------------------------------------------------- /src/page/pointer/DrivingPolicy.ts: -------------------------------------------------------------------------------- 1 | const policyArray = [ 2 | {label: '时间最少', key: 'LEAST_TIME', value: 0,}, 3 | {label: '最少花费', key: 'LEAST_FEE', value: 1,}, 4 | {label: '最短距离', key: 'LEAST_DISTANCE', value: 2,}, 5 | {label: '考虑实际路况', key: 'REAL_TRAFFIC', value: 4,}, 6 | // {label: '', key_service: 'MULTI_POLICIES', value: 5,}, 7 | // {label: '', key_service: 'HIGHWAY', value: 6,}, 8 | // {label: '', key_service: 'FEE_HIGHWAY', value: 7,}, 9 | // {label: '', key_service: 'FEE_TRAFFIC', value: 8,}, 10 | // {label: '', key_service: 'TRAFFIC_HIGHWAY', value: 9,}, 11 | ] 12 | 13 | let policyMap = new Map() 14 | policyArray.forEach(item => { 15 | policyMap.set(item.value, item.label) 16 | }) 17 | 18 | 19 | export { 20 | policyArray, policyMap 21 | } 22 | 23 | // 更新于 2023-04-12 24 | -------------------------------------------------------------------------------- /src/api/userApi.ts: -------------------------------------------------------------------------------- 1 | import {request} from "./request"; 2 | 3 | export default { 4 | login(requestData){return request('post', null, requestData, false, 'user/login')}, 5 | register(requestData) {return request('post', {}, requestData, false, 'user/register')}, 6 | add(requestData) {return request('post', {}, requestData, false, 'user/add')}, 7 | delete(requestData) {return request('delete', {}, requestData, false, 'user/delete')}, 8 | modify(requestData) {return request('put', {}, requestData, false, 'user/modify')}, 9 | detail(params) {return request('get', params, null, false, 'user/detail')}, 10 | list(requestData) {return request('post', null, requestData, false, 'user/list')}, 11 | changePassword(requestData) {return request('put', null, requestData, false, 'user/change-password')}, 12 | } 13 | -------------------------------------------------------------------------------- /src/page/route/DrivingPolicy.ts: -------------------------------------------------------------------------------- 1 | const policyArray = [ 2 | {label: '时间最少', key: 'LEAST_TIME', value: 0,}, 3 | {label: '最少花费', key: 'LEAST_FEE', value: 1,}, 4 | {label: '最短距离', key: 'LEAST_DISTANCE', value: 2,}, 5 | {label: '考虑实时路况', key: 'REAL_TRAFFIC', value: 4,}, 6 | {label: '综合考虑', key_service: 'MULTI_POLICIES', value: 5,}, 7 | {label: '优先高速', key_service: 'HIGHWAY', value: 6,}, 8 | {label: '经济且优先高速', key_service: 'FEE_HIGHWAY', value: 7,}, 9 | {label: '经济并考虑交通情况', key_service: 'FEE_TRAFFIC', value: 8,}, 10 | {label: '考虑交通并优先选择高速', key_service: 'TRAFFIC_HIGHWAY', value: 9,}, 11 | ] 12 | 13 | let policyMap = new Map() 14 | policyArray.forEach(item => { 15 | policyMap.set(item.value, item.label) 16 | }) 17 | 18 | 19 | export { 20 | policyArray, policyMap 21 | } 22 | 23 | // 更新于 2023-04-12 24 | -------------------------------------------------------------------------------- /src/page/other/map/dataPoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | {"type": "Feature", "properties": {"country": "山东", "time": "1949年10月6日", "flagName": "shandong", "type": 1}, "geometry": {"type": "Point", "coordinates": [117.132546, 36.693774]}}, 5 | {"type": "Feature", "properties": {"country": "蒙古", "time": "1949年10月16日", "flagName": "menggu", "type": 1}, "geometry": {"type": "Point", "coordinates": [103.453, 46.8528]}}, 6 | {"type": "Feature", "properties": {"country": "阿尔巴尼亚", "time": "1949年11月23日", "flagName": "aerbaniya", "type": 1}, "geometry": {"type": "Point", "coordinates": [20.1179, 41.1489]}}, 7 | {"type": "Feature", "properties": {"country": "保加利亚", "time": "1949年10月4日", "flagName": "baojialiya", "type": 1}, "geometry": {"type": "Point", "coordinates": [25.3611, 42.7454]}} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | }, 8 | 9 | "target": "ES2020", 10 | "useDefineForClassFields": true, 11 | "module": "ESNext", 12 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 13 | "skipLibCheck": true, 14 | 15 | /* Bundler mode */ 16 | "moduleResolution": "bundler", 17 | "allowImportingTsExtensions": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve", 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "allowSyntheticDefaultImports": true, 29 | }, 30 | 31 | "include": ["src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 32 | "references": [{ "path": "./tsconfig.node.json" }] 33 | } 34 | -------------------------------------------------------------------------------- /src/page/other/map/dataLine.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | {"type": "Feature", "properties": {"country": "山东", "time": "1949年10月6日", "flagName": "shandong", "type": 1}, "geometry": {"type": "LineString", "coordinates": [[121.504673, 25.046711], [117.132546, 36.693774]]}}, 5 | {"type": "Feature", "properties": {"country": "蒙古", "time": "1949年10月16日", "flagName": "menggu", "type": 1}, "geometry": {"type": "LineString", "coordinates": [[121.504673, 25.046711], [103.453, 46.8528]]}}, 6 | {"type": "Feature", "properties": {"country": "阿尔巴尼亚", "time": "1949年11月23日", "flagName": "aerbaniya", "type": 1}, "geometry": {"type": "LineString", "coordinates": [[121.504673, 25.046711], [20.1179, 41.1489]]}}, 7 | {"type": "Feature", "properties": {"country": "保加利亚", "time": "1949年10月4日", "flagName": "baojialiya", "type": 1}, "geometry": {"type": "LineString", "coordinates": [[121.504673, 25.046711], [25.3611, 42.7454]]}} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/scss/_overwrite-el-ui.scss: -------------------------------------------------------------------------------- 1 | //input[type=number] { 2 | // padding-right: 0!important; 3 | //} 4 | // 5 | //.el-input__inner{ 6 | // padding-left: 6px !important; 7 | //} 8 | //.el-textarea__inner{ 9 | // padding: 5px 8px !important; 10 | //} 11 | 12 | .el-table .cell { 13 | line-height: 18px; 14 | } 15 | 16 | // TAB 17 | .el-tabs__item{ 18 | height: 30px !important; 19 | line-height: 30px !important; 20 | font-size: $fz-normal; 21 | } 22 | .el-tabs__content{ 23 | padding: 0 !important; 24 | } 25 | 26 | .el-tabs--border-card>.el-tabs__header{ 27 | height: 30px !important; 28 | margin-bottom: 0 !important; 29 | border-bottom: none; 30 | } 31 | 32 | 33 | .search-bar{ 34 | margin-left: 20px; 35 | margin-right: 10px; 36 | padding: 0 10px; 37 | align-items: center; 38 | height: 50px; 39 | display: flex; 40 | justify-content: space-between; 41 | .el-form-item{ 42 | margin-right: 15px !important; 43 | margin-bottom: 0; 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/mapConfig.ts: -------------------------------------------------------------------------------- 1 | const key_web_js = '581591b581149549d9035d039e83e368' // web js key 2 | const key_service = '401d946dc1152f0e1f110928ecc07a13' // web服务 key 3 | // 管理地址在:https://console.amap.com/dev/key/app 4 | 5 | // 七牛云 与 《标题日记》共用一个 仓库 6 | // 地址: https://portal.qiniu.com/kodo/overview 7 | const qiniu_img_base_url = 'http://apple-image.kylebing.cn/' // 空间域名,最后面带 `/` 8 | const qiniu_bucket_name = 'apple-image' // 七牛云对象存储空间的名称 9 | const thumbnail200_suffix = 'thumbnail_200px' // 七牛云缩略图样式名 200x200px 质量75 10 | const thumbnail600_suffix = 'thumbnail_600px' // 七牛云缩略图样式名 600x600px 质量75 11 | const thumbnail1000_suffix = 'thumbnail_1000px' // 七牛云缩略图样式名 1000x1000px 质量75 12 | const thumbnail1500_suffix = 'thumbnail_1500px' // 七牛云缩略图样式名 1500x1500px 质量75 13 | 14 | export { 15 | key_web_js, 16 | key_service, 17 | qiniu_img_base_url, 18 | qiniu_bucket_name, 19 | thumbnail200_suffix, 20 | thumbnail600_suffix, 21 | thumbnail1000_suffix, 22 | thumbnail1500_suffix, 23 | } 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 路书 9 | 10 | 11 | 12 |
13 | 14 | 15 | 25 | 30 | 31 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import zipPack from "vite-plugin-zip-pack" // make dist.zip file 4 | 5 | import {resolve} from "path" 6 | import {dateFormatter} from "./src/utility"; 7 | 8 | const timeStringNow = dateFormatter(new Date(), 'yyyy-MM-dd hh-mm') 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig({ 12 | base: './', 13 | plugins: [ 14 | vue(), 15 | zipPack({ 16 | inDir: 'dist', 17 | outDir: 'archive', 18 | outFileName: `map-${timeStringNow}.zip`, 19 | pathPrefix: '' 20 | }), 21 | ], 22 | resolve: { 23 | alias: { 24 | '@': resolve(__dirname, 'src'), 25 | }, 26 | }, 27 | server: { 28 | proxy: { 29 | '/dev': { 30 | target: 'http://kylebing.cn/portal/', 31 | // target: 'http://localhost:3000/', 32 | changeOrigin: true, 33 | rewrite: (path) => path.replace(/^\/dev/, '/'), 34 | }, 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import {EntityAuthorization} from "@/utility.ts"; 3 | 4 | export const useProjectStore = defineStore('storeProject', { 5 | state: ()=>({ 6 | navWidth: 200, // 导航宽度 7 | windowInsets: { // 窗口数据 8 | height: 0, 9 | width: 0 10 | }, 11 | contentInsets: { 12 | heightToolbar: 60, // toolbar 高度 13 | heightPager: 50, // pagination 高度 14 | heightContent: 0, // 内容高度 15 | widthContent: 0, // 内容宽度 16 | }, 17 | isNavMenuFold: false, // navMenu 是否折叠状态 18 | isShowFloatingMenuBtn: false, // 是否显示移动端的菜单切换按钮 19 | authorization: null as EntityAuthorization, // authorization 20 | 21 | isShowUserSelfLocation: false, // 是否显示用户自己的位置, http 下无用,https 才有用 22 | }), 23 | getters: { 24 | isInPortraitMode (state) { 25 | return state.windowInsets.height > state.windowInsets.width 26 | }, 27 | isAdmin (state) { 28 | return state.authorization && state.authorization.group_id === 1 29 | }, 30 | } 31 | }) 32 | 33 | -------------------------------------------------------------------------------- /src/layout/FooterPagination.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | 36 | 45 | -------------------------------------------------------------------------------- /src/page/route/Route.ts: -------------------------------------------------------------------------------- 1 | interface EntityRoute { 2 | id?: number, 3 | name: string, // 路线名称 4 | area: string, // 地址位置 5 | road_type: string, // 路面类型 6 | note: string, // 备注 7 | is_public: 0|1, // 是否共享 0 否 1 是 8 | 9 | video_link?: string, // 视频链接 10 | thumb_up?: number, // 点赞数 11 | seasonsArray?: Array, // 季节数组 12 | seasons?: string, // 骑行季节 13 | paths?: string, // 路线节点 14 | pathArray?: Array, 15 | date_modify?: string, // 编辑时间 16 | date_init?: string, // 创建时间 17 | uid?: number, // user ID 18 | policy?: number, // 路线规划策略 19 | 20 | distance?: number, 21 | nickname?: string 22 | time?: string 23 | } 24 | 25 | interface EntityRoutePoint { 26 | type: string, // 颜色类别,为了匹配 EntityPointerPoint 的属性,共用一个方法 27 | position: [number, number], // 地址 lng,lat 28 | note: string, // 标点备注 29 | name: string, // 标点名称 30 | img?: string // 图片 http 路径 31 | } 32 | 33 | export { 34 | type EntityRoute, 35 | type EntityRoutePoint 36 | } 37 | -------------------------------------------------------------------------------- /src/scss/_reset.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | border: 0; 5 | font-family: inherit; 6 | color: inherit; 7 | -webkit-box-sizing: border-box; 8 | -moz-box-sizing: border-box; 9 | box-sizing: border-box; } 10 | 11 | body { 12 | font-family: "PingFang SC", "Microsoft Yahei UI", "Microsoft Yahei", "Helvetica", sans-serif; 13 | width: 100%; 14 | margin: 0 auto; } 15 | 16 | a { 17 | text-decoration: none; } 18 | 19 | table { 20 | border-collapse: collapse; } 21 | 22 | code, 23 | kbd, 24 | pre, 25 | samp, 26 | tt { 27 | font-family: "Courier New", Courier, monospace; } 28 | 29 | :focus, 30 | :active { 31 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 32 | outline: none; } 33 | 34 | ::-webkit-scrollbar { 35 | z-index: 50; 36 | width: 3px; 37 | height: 3px; } 38 | 39 | ::-webkit-scrollbar-track { 40 | border-bottom: 1px solid #eeeeee; 41 | border-right: 1px solid #eeeeee; 42 | background-color: rgba(0, 0, 0, 0); } 43 | 44 | ::-webkit-scrollbar-thumb { 45 | -webkit-border-radius: 5px; 46 | -moz-border-radius: 5px; 47 | border-radius: 5px; 48 | background-color: #eee; 49 | transition: all .2s; 50 | height: 3px; } 51 | 52 | :hover::-webkit-scrollbar-thumb { 53 | background-color: #dfdfdf; 54 | transition: all .2s; } 55 | 56 | ::-webkit-scrollbar-button { 57 | display: none; } 58 | 59 | ::-webkit-scrollbar-corner { 60 | display: none; } -------------------------------------------------------------------------------- /src/layout/FloatingButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | 36 | 60 | -------------------------------------------------------------------------------- /src/layout/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 56 | -------------------------------------------------------------------------------- /src/layout/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 22 | 23 | 55 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /src/page/pointer/Pointer.ts: -------------------------------------------------------------------------------- 1 | export interface EntityPointer { 2 | id?: number, // ID 3 | name: string, // *点图名 4 | note: string, // 简介 5 | uid?: number, // 创建人 6 | date_create?: string, // 创建时间 7 | date_modify?: string, // 编辑时间 8 | area: string, // *地域 9 | thumb_up: number, // *点赞数 10 | is_public: number, // *是否公开 11 | visit_count?: number, // 访问次数 12 | video_link: string, // 路径视频演示 13 | pointers: string, // *路径点 json 字符串 14 | pointer_array?: Array, // *路径点 15 | 16 | nickname: string, // 创建用户 17 | username: string, // 用户名 18 | } 19 | 20 | export interface EntityPointerPoint{ 21 | note: string, 22 | img?: string, 23 | position: [number, number], 24 | type: string, 25 | name: string 26 | } 27 | 28 | export enum EnumPointerType { 29 | "yellow" = "yellow", 30 | "orange" = "orange", 31 | "red" = "red", 32 | "green" = "green", 33 | "blue" = "blue", 34 | } 35 | export const EnumPointerTypeMap = new Map([ 36 | [EnumPointerType.yellow, "黄色"], 37 | [EnumPointerType.orange, "橙色"], 38 | [EnumPointerType.red, "红色"], 39 | [EnumPointerType.green, "绿色"], 40 | [EnumPointerType.blue, "蓝色"], 41 | ]) 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riding-map", 3 | "date-update": "2025.10.23", 4 | "version": "1.0.1", 5 | "private": true, 6 | "github": "https://github.com/KyleBing/map", 7 | "author": { 8 | "name": "KyleBing", 9 | "email": "kylebing@163.com", 10 | "homepage": "http://kylebing.cn" 11 | }, 12 | "scripts": { 13 | "dev": "vite --host", 14 | "build": "tsc && vite build", 15 | "preview": "vite preview" 16 | }, 17 | "dependencies": { 18 | "@amap/amap-jsapi-loader": "^1.0.1", 19 | "@element-plus/icons-vue": "^2.1.0", 20 | "axios": "^1.6.3", 21 | "clipboard": "^2.0.11", 22 | "countup.js": "^2.8.0", 23 | "echarts": "^5.5.0", 24 | "js-base64": "^3.7.5", 25 | "moment": "^2.29.4", 26 | "pinia": "^2.1.7", 27 | "sass-loader": "^13.3.2", 28 | "vue": "^3.4.27", 29 | "vue-router": "^4.2.5", 30 | "animate-bg-canvas": "^0.1.2", 31 | "animate-heart-canvas": "^0.1.9", 32 | "core-js": "^3.8.3", 33 | "element-plus": "^2.6.0", 34 | "fast-xml-parser": "^4.3.3", 35 | "filemanager-webpack-plugin": "^7.0.0", 36 | "marked": "^4.2.4", 37 | "qiniu-js": "^3.4.1", 38 | "geosvg": "^1.2.3" 39 | }, 40 | "devDependencies": { 41 | "@types/mockjs": "^1.0.10", 42 | "@types/spark-md5": "^3.0.4", 43 | "@types/amap-js-api": "^1.4.16", 44 | "@vitejs/plugin-vue": "^4.2.3", 45 | "sass": "^1.69.5", 46 | "typescript": "^5.0.2", 47 | "vite": "^4.4.5", 48 | "vite-plugin-mock": "^3.0.2", 49 | "vite-plugin-zip-pack": "^1.0.7", 50 | "vue-tsc": "^1.8.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $green : #4CD964; 3 | $cyan : #5AC8FA; 4 | $blue : #007AFF; 5 | $purple : #5856D6; 6 | $magenta : #FF2D70; 7 | $red : #FF3B30; 8 | $orange : #FF9500; 9 | $yellow : #FFCC00; 10 | $gray : #8E8E93; 11 | 12 | // ELEMENT COLORS 13 | $color-main : $magenta; 14 | $border-color-nav: transparentize($color-main, 0.8); 15 | $border-light: #f2f2f2; 16 | 17 | // FONT SIZE 18 | $fz-title: 16px; 19 | $fz-info: 30px; 20 | $fz-normal: 13px; 21 | $fz-small: 11px; 22 | 23 | $bg-active: transparentize($color-main, 0.9); 24 | $bg-second: transparentize(black, 0.95); 25 | $bg-light: #f7f7f7; 26 | 27 | 28 | $color-primary : #409EFF; 29 | $color-success : #67C23A; 30 | $color-warning : #E6A23C; 31 | $color-danger : #F56C6C; 32 | $color-info : #909399; 33 | 34 | $border-normal: #eee; 35 | 36 | $text-main : #333; 37 | $text-description: #666; 38 | $text-subtitle : #848484; 39 | 40 | // USER CENTER 41 | $gradient-blue : $cyan, $blue; 42 | $gradient-gray : $gray, darken($gray, 15%); 43 | $gradient-green : $green, darken($green, 15%); 44 | $gradient-orange : $orange, darken($orange, 5%); 45 | $gradient-red : $red, darken($red, 15%); 46 | $gradient-purple : $purple, darken($purple, 15%); 47 | $gradient-magenta : $magenta, darken($magenta, 15%); 48 | $gradient-yellow : $yellow, darken($yellow, 15%); 49 | $gradient-time : #79C1F0, #7E87EF; 50 | 51 | 52 | $radius: 5px; 53 | 54 | 55 | $screen-width-threshold: 500px; 56 | 57 | $width-float-btn: 45px; // 悬浮的按钮宽度 58 | -------------------------------------------------------------------------------- /src/page/index/Index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | 28 | 56 | -------------------------------------------------------------------------------- /src/page/other/map/province.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "北京市", "center": "116.407394,39.904211" }, 3 | { "name": "天津市", "center": "117.200983,39.084158" }, 4 | { "name": "河北省", "center": "114.530235,38.037433" }, 5 | { "name": "山西省", "center": "112.562678,37.873499" }, 6 | { "name": "内蒙古自治区", "center": "111.76629,40.81739" }, 7 | { "name": "辽宁省", "center": "123.431382,41.836175" }, 8 | { "name": "吉林省", "center": "125.32568,43.897016" }, 9 | { "name": "黑龙江省", "center": "126.661665,45.742366" }, 10 | { "name": "上海市", "center": "121.473662,31.230372" }, 11 | { "name": "江苏省", "center": "118.762765,32.060875" }, 12 | { "name": "浙江省", "center": "120.152585,30.266597" }, 13 | { "name": "安徽省", "center": "117.329949,31.733806" }, 14 | { "name": "福建省", "center": "119.295143,26.100779" }, 15 | { "name": "江西省", "center": "115.81635,28.63666" }, 16 | { "name": "山东省", "center": "117.019915,36.671156" }, 17 | { "name": "河南省", "center": "113.753394,34.765869" }, 18 | { "name": "湖北省", "center": "114.341745,30.546557" }, 19 | { "name": "湖南省", "center": "112.9836,28.112743" }, 20 | { "name": "广东省", "center": "113.26641,23.132324" }, 21 | { "name": "广西壮族自治区", "center": "108.327546,22.815478" }, 22 | { "name": "海南省", "center": "110.349228,20.017377" }, 23 | { "name": "重庆市", "center": "106.551643,29.562849" }, 24 | { "name": "四川省", "center": "104.075809,30.651239" }, 25 | { "name": "贵州省", "center": "106.70546,26.600055" }, 26 | { "name": "云南省", "center": "102.710002,25.045806" }, 27 | { "name": "西藏自治区", "center": "91.117525,29.647535" }, 28 | { "name": "陕西省", "center": "108.954347,34.265502" }, 29 | { "name": "甘肃省", "center": "103.826447,36.05956" }, 30 | { "name": "青海省", "center": "101.780268,36.620939" }, 31 | { "name": "宁夏回族自治区", "center": "106.259126,38.472641" }, 32 | { "name": "新疆维吾尔自治区", "center": "87.627704,43.793026" }, 33 | { "name": "香港特别行政区", "center": "114.171203,22.277468" }, 34 | { "name": "澳门特别行政区", "center": "113.543028,22.186835" } 35 | ] 36 | -------------------------------------------------------------------------------- /src/lib/colors.ts: -------------------------------------------------------------------------------- 1 | const colors = { 2 | green: "#4CD964", 3 | syan: "#5AC8FA", 4 | blue: "#007AFF", 5 | purple: "#5856D6", 6 | magenta: "#FF2D70", 7 | red: "#FF3B30", 8 | orange: "#FF9500", 9 | yellow: "#FFCC00", 10 | gray: "#8E8E93", 11 | } 12 | 13 | const ColorsProvince = { 14 | '370600': {color: 'rgb(190,194,161)', name: "烟台市"}, 15 | '371300': {color: 'rgb(237,236,208)', name: "临沂市"}, 16 | '370200': {color: 'rgb(222,148,111)', name: "青岛市"}, 17 | '370300': {color: 'rgb(103,104,099)', name: "淄博市"}, 18 | '370700': {color: 'rgb(201,166,085)', name: "潍坊市"}, 19 | '371100': {color: 'rgb(113,182,153)', name: "日照市"}, 20 | '371500': {color: 'rgb(212,227,206)', name: "聊城市"}, 21 | '370400': {color: 'rgb(208,198,170)', name: "枣庄市"}, 22 | '370100': {color: 'rgb(205,087,085)', name: "济南市"}, 23 | '371700': {color: 'rgb(214,201,193)', name: "菏泽市"}, 24 | '370900': {color: 'rgb(212,144,167)', name: "泰安市"}, 25 | '371400': {color: 'rgb(242,242,218)', name: "德州市"}, 26 | '370800': {color: 'rgb(246,194,180)', name: "济宁市"}, 27 | '371600': {color: 'rgb(200,239,186)', name: "滨州市"}, 28 | '370500': {color: 'rgb(178,215,221)', name: "东营市"}, 29 | '371000': {color: 'rgb(146,128,140)', name: "威海市"}, 30 | } 31 | 32 | const colorArray = [ 33 | "#4CD964", 34 | "#5AC8FA", 35 | "#007AFF", 36 | "#5856D6", 37 | "#FF2D70", 38 | "#FF3B30", 39 | "#FF9500", 40 | "#FFCC00", 41 | "#8E8E93", 42 | ] 43 | 44 | const colorsRGB = [ 45 | 'rgb(190,194,161)', 46 | 'rgb(237,236,208)', 47 | 'rgb(222,148,111)', 48 | 'rgb(103,104,099)', 49 | 'rgb(201,166,085)', 50 | 'rgb(113,182,153)', 51 | 'rgb(212,227,206)', 52 | 'rgb(208,198,170)', 53 | 'rgb(205,087,085)', 54 | 'rgb(214,201,193)', 55 | 'rgb(212,144,167)', 56 | 'rgb(242,242,218)', 57 | 'rgb(246,194,180)', 58 | 'rgb(200,239,186)', 59 | 'rgb(178,215,221)', 60 | 'rgb(146,128,140)', 61 | ] 62 | export { 63 | colors, colorsRGB, colorArray, ColorsProvince 64 | } 65 | -------------------------------------------------------------------------------- /src/page/pointer/components/PointerDetailPanel.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 64 | 65 | 68 | -------------------------------------------------------------------------------- /src/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 43 | 44 | 75 | -------------------------------------------------------------------------------- /src/layout/Copyright.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | 46 | 80 | -------------------------------------------------------------------------------- /src/scss/_utility.scss: -------------------------------------------------------------------------------- 1 | // box-shadow 2 | @mixin box-shadow($value...){ 3 | -webkit-box-shadow: $value; 4 | -moz-box-shadow: $value; 5 | box-shadow: $value; 6 | } 7 | 8 | // border-radius 9 | @mixin border-radius($corner...){ 10 | -webkit-border-radius: $corner; 11 | -moz-border-radius: $corner; 12 | border-radius: $corner; 13 | } 14 | 15 | @mixin clearfix(){ 16 | &:after{ 17 | content: ''; 18 | display: block; 19 | clear: both; 20 | visibility: hidden; 21 | } 22 | } 23 | 24 | @mixin transform($value){ 25 | -webkit-transform: $value; 26 | -moz-transform: $value; 27 | -ms-transform: $value; 28 | -o-transform: $value; 29 | transform: $value; 30 | } 31 | 32 | @mixin transition($value...){ 33 | -webkit-transition: $value; 34 | -moz-transition: $value; 35 | -ms-transition: $value; 36 | -o-transition: $value; 37 | transition: $value; 38 | } 39 | 40 | @mixin animation($value){ 41 | animation: $value; 42 | -webkit-animation: $value; 43 | } 44 | 45 | @mixin linear-gradient($direct, $colors){ 46 | background: linear-gradient($direct, $colors); 47 | background: -webkit-linear-gradient($direct, $colors); 48 | background: -moz-linear-gradient($direct, $colors); 49 | } 50 | 51 | @mixin backdrop-filter($value){ 52 | backdrop-filter: $value ; 53 | -webkit-backdrop-filter: $value; 54 | } 55 | 56 | // 1像素 57 | @mixin divider-1px(){ 58 | content:''; 59 | height: 1px; 60 | display: block; 61 | width: 100%; 62 | position: absolute; 63 | background-color: $border-normal; 64 | @include transform(scaleY(.5)) 65 | } 66 | 67 | /* 68 | Extension 69 | */ 70 | 71 | .unselectable { 72 | -webkit-user-select: none; 73 | -moz-user-select: none; 74 | -ms-user-select: none; 75 | user-select: none; 76 | } 77 | 78 | .btn-like{ 79 | cursor: pointer; 80 | @extend .unselectable; 81 | &:active{ 82 | @include transform(translateY(2px)) 83 | } 84 | } 85 | 86 | .card{ 87 | padding: 6px 8px; 88 | overflow: hidden; 89 | background-color: transparentize(white, 0.2); 90 | backdrop-filter: blur(5px) saturate(130%); 91 | @include border-radius(8px); 92 | @include box-shadow(1px 1px 3px rgba(0,0,0,0.2)); 93 | } 94 | 95 | .text-center{text-align: center;} 96 | .text-left{text-align: left;} 97 | .text-right{text-align: right;} 98 | -------------------------------------------------------------------------------- /src/layout/Aside.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 47 | 48 | 80 | -------------------------------------------------------------------------------- /src/api/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {getAuthorization} from "@/utility"; 3 | import {ServerResponse} from "@/api/ServerResponse.ts"; 4 | import {ElLoading, ElMessage} from "element-plus"; 5 | 6 | 7 | const LOADING_OPTION = { 8 | lock: true, 9 | text: "载入中,请稍候...", 10 | background: "rgba(0, 0, 0, 0.3)" 11 | } 12 | 13 | const BASE_URL: string = process.env.NODE_ENV === 'development' ? '/dev/' : 'http://kylebing.cn/portal/' // 生产环境时是 ../portal 14 | 15 | function request( 16 | method: 'get' | 'post' | 'put' | 'delete', 17 | params: any, 18 | requestData: any, 19 | showLoading = false, 20 | url: string 21 | ): Promise { 22 | let layerLoading = null 23 | if (showLoading) layerLoading = ElLoading.service(LOADING_OPTION) 24 | 25 | let headers = {} 26 | /* 27 | * 所有 requestData 都会自动添加 authorization 信息 28 | * 给 requestData 添加 authorization 内部的数据: username email uid 等等 29 | * */ 30 | if (url !== 'user/Login' && url !== 'user/Register') { // 注册和登录时不添加 Token 数据 31 | Object.assign(headers, { 32 | 'Diary-Token': getAuthorization() && getAuthorization().token, 33 | 'Diary-Uid': getAuthorization() && getAuthorization().uid 34 | }) 35 | } 36 | 37 | return new Promise((resolve, reject) => { 38 | axios({ 39 | url: BASE_URL + url, 40 | method, 41 | data: requestData, 42 | params, 43 | headers, 44 | withCredentials: true 45 | }) 46 | .then(res => { 47 | if (showLoading && layerLoading) layerLoading.close() 48 | if (res.status === 200) { 49 | if (res.data.success) { 50 | resolve(res.data) 51 | } else { 52 | ElMessage.error({ 53 | message: res.data.message || 'Error' 54 | }) 55 | reject(res.data) 56 | } 57 | } else { 58 | reject(res.data) 59 | console.log('request err: ', res.data) // 如果演示模式,不用显示网络请求错误 60 | } 61 | }) 62 | .catch(err => { 63 | if (showLoading && layerLoading) layerLoading.close() 64 | ElMessage.error({ 65 | message: err.message 66 | }) 67 | console.log(err, err.message) 68 | reject(err) 69 | }) 70 | }) 71 | } 72 | 73 | 74 | export { 75 | request 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 路书 2 | 骑行路线规划应用 3 | 4 | 一个基于 [高德地图API](https://lbs.amap.com/api/javascript-api-v2/documentation) 制作的地图 web 应用,可以分享路线、查看 gpx 路径、地图信息点分享、各市区县布局等。 5 | 6 | > **注意** 7 | 高德地图api只用作开发测试用,并不能用于实际使用,如果持续使用,高德官方会打电话给你,督促你购买商业使用授权,每年5w,不缴纳就会过段时间给你把服务停了。 8 | 所以在未获取到授权之前,不要用在商业项目上,不然挺麻烦的。 9 | > 如果你想找一个免费的地图使用,可以看一下 [OpenStreetMap](https://www.openstreetmap.org/) OSM 10 | 是一个可以免费使用的地图,地图信息主要靠所有网友自行添加维护。 11 | > 它的用法也非常简单,我准备下一步的地图应用在这个基础上做,可以看一下例子: https://github.com/KyleBing/map-OSM 12 | 在使用它的时候,需要遵循一定的使用要求。 13 | 14 | 15 | 16 | 路线分享 17 | Screenshot 2024-01-23 at 17 45 28 18 | 19 | gpx 路径 展示 20 | GPX 21 | 22 | gpx 3D 路径展示 23 | ![2024-05-06 13-52-05 2024-05-06 13_55_08](https://github.com/KyleBing/map/assets/12215982/1bd3033e-84cf-4d56-9b3f-22e6c46c94b8) 24 | 25 | 数据点展示 26 | Screenshot 2024-01-23 at 17 39 57 27 | 28 | 范围标记 29 | Screenshot 2024-01-23 at 17 40 29 30 | 31 | 市内区县展示 32 | Screenshot 2024-01-23 at 17 40 42 33 | 34 | --- 35 | 36 | # 开发 37 | 38 | ## 一、获取 高德地图 API key 39 | **注意** 40 | 高德地图api只用作开发测试用,并不能用于实际使用,如果持续使用,高德官方会打电话给你,督促你购买商业使用授权,每年5w 41 | 42 | ## 二、使用 43 | 44 | 修改 `/src/mapConfig.js` 中的高德地图开发 key,获取地址: [https://console.amap.com/dev/key/app](https://console.amap.com/dev/key/app) 45 | 46 | 有两个,别选错了 47 | 48 | 49 | 开发api key 版本 50 | 51 | 52 | ```js 53 | const key_web_js = '' // web js key 54 | const key_service = '' // web服务 key 55 | 56 | export { 57 | key_web_js, 58 | key_service 59 | } 60 | ``` 61 | 62 | Web JS 在使用的时候还需要在服务器中设置对应的安全密钥,具体参见官方文档: 63 | > [JS API 安全密钥使用 https://lbs.amap.com/api/javascript-api-v2/guide/abc/jscode](https://lbs.amap.com/api/javascript-api-v2/guide/abc/jscode) 64 | 65 | ## 三、后台程序 66 | 67 | 后台:[https://github.com/KyleBing/portal](https://github.com/KyleBing/portal) 68 | 69 | 70 | ## 四、用到的技术 71 | - `vue3` + `pinia` + `router` 72 | - `ts` 73 | - 高德 API 2.0 74 | 75 | ## 五、历程 76 | - 2021-06-28 init `vue2` 77 | - 2024-07-26 `vite` + `ts` + `vue3` 78 | 79 | 80 | ## 六、todo 81 | - [x] 点过多时,列表滚动 `2025-04-18` 82 | -------------------------------------------------------------------------------- /src/page/route/components/RouteDetailPanel.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 88 | 89 | 92 | -------------------------------------------------------------------------------- /src/page/SearchPanel.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 89 | 90 | 96 | 97 | -------------------------------------------------------------------------------- /src/page/pointer/components/PointerListPanel.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 40 | 41 | 109 | -------------------------------------------------------------------------------- /src/scss/_markdown.scss: -------------------------------------------------------------------------------- 1 | $gap: 15px; 2 | 3 | .markdown{ 4 | line-height: 1.6; 5 | color: $text-main; 6 | font-size: 12px; 7 | 8 | hr{ 9 | height: 1px; 10 | background-color: $border-normal; 11 | width: 100%; 12 | margin: 20px 0; 13 | } 14 | 15 | p{ 16 | line-height: 1.5; 17 | margin-bottom: 5px; 18 | } 19 | 20 | img{ 21 | border: 1px solid $border-normal; 22 | max-width: 100%; 23 | background-color: white; 24 | @include border-radius(3px); 25 | } 26 | 27 | @for $item from 1 through 7 { 28 | h#{$item}{ 29 | margin-top: (7-$item)*5px; 30 | margin-bottom: (7-$item)*2px; 31 | font-size: 12px + (7-$item)*1.5; 32 | } 33 | } 34 | 35 | pre{ 36 | margin-bottom: $gap; 37 | font-size: $fz-small; 38 | padding: $gap $gap + 5; 39 | background-color: $bg-active; 40 | code{ 41 | padding: 0; 42 | background-color: $bg-active; 43 | } 44 | } 45 | 46 | code{ 47 | padding: 2px 5px; 48 | font-size: 13.5px; 49 | color: #333; 50 | background-color: $bg-active; 51 | @include border-radius(2px); 52 | } 53 | 54 | table{ 55 | margin-bottom: $gap; 56 | border: 1px solid $border-normal; 57 | tr{ 58 | th{ 59 | text-align: left; 60 | min-width: 50px; 61 | padding: 4px 10px; 62 | border: 1px solid $border-normal; 63 | background-color: $bg-active; 64 | &[align=left]{ 65 | text-align: left; 66 | } 67 | &[align=right]{ 68 | text-align: right; 69 | } 70 | &[align=center]{ 71 | text-align: center; 72 | } 73 | } 74 | td{ 75 | text-align: left; 76 | border: 1px solid $border-normal; 77 | padding: 3px 10px; 78 | &[align=left]{ 79 | text-align: left; 80 | } 81 | &[align=right]{ 82 | text-align: right; 83 | } 84 | &[align=center]{ 85 | text-align: center; 86 | } 87 | } 88 | } 89 | } 90 | 91 | input[type=checkbox]{ 92 | display: inline-block; 93 | -webkit-appearance: checkbox !important; 94 | } 95 | 96 | ol, ul{ 97 | margin-left: $gap; 98 | margin-bottom: $gap; 99 | li{ 100 | margin-left: 20px; 101 | line-height: 1.5; 102 | margin-bottom: 2px; 103 | } 104 | } 105 | blockquote{ 106 | margin-top: 15px; 107 | margin-bottom: 15px; 108 | padding-top: 10px; 109 | padding-bottom: 10px; 110 | border-left: 3px solid transparentize(black, 0.8); 111 | padding-left: 18px; 112 | background-color: #f5f5f5; 113 | > *:last-child{ 114 | margin-bottom: 0; 115 | } 116 | } 117 | 118 | a{ 119 | color: $orange; 120 | text-decoration: underline; 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/page/MyMapLib.ts: -------------------------------------------------------------------------------- 1 | import {thumbnail1000_suffix, thumbnail1500_suffix} from "../mapConfig"; 2 | 3 | /** 4 | * 生成 Marker.note 5 | * @param note 注释 6 | * @param img 图片链接 7 | * @param type 类别:颜色 8 | * @param name 标题 9 | * @param index 序号 10 | */ 11 | function generateMarkerContent( 12 | name: string, 13 | note?: string, 14 | img?: string, 15 | type?: string, 16 | index?: number 17 | ) { 18 | if (img && note){ 19 | return ` 20 |
21 |
22 |
${index! + 1}
23 |
${name}
24 |
25 |
26 |
${note.replace(/\n/g, '
')}
27 |
28 | 29 | view 30 | 31 |
32 |
33 |
` 34 | } else if (img) { 35 | return ` 36 |
37 |
38 |
${index! + 1}
39 |
${name}
40 |
41 |
42 |
43 | 44 | view 45 | 46 |
47 |
48 |
` 49 | } else if (note){ 50 | return ` 51 |
52 |
53 |
${index! + 1}
54 |
${name}
55 |
56 |
57 |
${note.replace(/\n/g, '
')}
58 |
59 |
` 60 | } else { 61 | return ` 62 |
63 |
64 |
${index! + 1}
65 |
${name}
66 |
67 |
` 68 | } 69 | } 70 | 71 | 72 | /** 73 | * 获取区域对角线的两点坐标,即这个区域内的最小坐标值和最大坐标值 74 | * 75 | * @return Array {min:number[a,b], max:number[c,d]} 76 | * @param pointerArray 77 | */ 78 | function getMaxBoundsPointer(pointerArray: Array<[number, number]>): {min: [number,number], max: [number,number]} { 79 | let lngArray = pointerArray.map(item => item[0]) 80 | let latArray = pointerArray.map(item => item[1]) 81 | 82 | return { 83 | min: [Math.min(...lngArray), Math.min(...latArray)], 84 | max: [Math.max(...lngArray), Math.max(...latArray)], 85 | } 86 | } 87 | 88 | 89 | export { 90 | generateMarkerContent, 91 | getMaxBoundsPointer 92 | } 93 | -------------------------------------------------------------------------------- /src/page/info/highwayXueye/HighwayXueye.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 111 | 112 | 118 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | @import "reset"; 4 | @import "plugin"; 5 | @import "gutter"; 6 | @import "animate"; 7 | @import "markdown"; 8 | @import "marker"; // map marker 9 | 10 | body{ 11 | background-color: #f2f2f2; 12 | } 13 | 14 | .header{ 15 | line-height: 60px; 16 | } 17 | 18 | body{ 19 | position: relative; 20 | } 21 | 22 | .home{ 23 | @include box-shadow(2px 2px 5px rgba(121, 85, 72, 0.39)); 24 | padding: 5px 8px; 25 | @include border-radius(5px); 26 | position: fixed; 27 | top: 20px; 28 | right: 20px; 29 | font-size: 0.8rem; 30 | background-color: white; 31 | cursor: pointer; 32 | } 33 | 34 | #panel{ 35 | position: absolute; 36 | top: 60px; 37 | right: 60px; 38 | bottom: 60px; 39 | } 40 | 41 | .textarea{ 42 | padding: 2px 3px; 43 | min-height: 30px; 44 | border: 1px solid transparentize(black, 0.8); 45 | @include border-radius(3px); 46 | &:hover{ 47 | outline: 2px solid transparentize($color-main, 0.5); 48 | //background-color: transparentize($color-main, 0.9); 49 | background-color: white; 50 | cursor: text; 51 | border-color: $color-main; 52 | } 53 | } 54 | 55 | 56 | .menu{ 57 | overflow: hidden; 58 | background-color: white; 59 | top: 30px; 60 | width: 150px; 61 | @include border-radius(8px); 62 | } 63 | .menu-item{ 64 | @extend .unselectable; 65 | cursor: pointer; 66 | border-bottom: 1px solid $border-normal; 67 | padding: 5px 10px 5px 30px; 68 | &:last-child{ 69 | border: none; 70 | } 71 | &:hover{ 72 | background-color: rgba(0,0,0,0.05); 73 | } 74 | &:active{ 75 | transform: translateY(2px); 76 | } 77 | } 78 | 79 | 80 | .button-center{ 81 | display: flex; 82 | justify-content: center; 83 | align-items: center; 84 | } 85 | 86 | 87 | .btn-narrow{ 88 | padding: 7px; 89 | } 90 | 91 | .input-focus{ 92 | input{ 93 | &:focus{ 94 | animation-name: focus; 95 | } 96 | } 97 | } 98 | 99 | @-webkit-keyframes focus { 100 | from {outline-width: 5px;} 101 | to {outline-width: 0;} 102 | } 103 | @keyframes focus { 104 | from {outline-width: 5px;} 105 | to {outline-width: 0;} 106 | } 107 | 108 | 109 | 110 | .table-date{ 111 | font-family: "JetBrains Mono", sans-serif; 112 | line-height: 1; 113 | .create-date{} 114 | .modify-date{ 115 | color: $orange; 116 | } 117 | } 118 | 119 | 120 | // 操作面板中 table 内部的一些操作按钮 121 | $height-btn: 28px; 122 | .lnglat{ 123 | cursor: pointer; 124 | flex-shrink: 0; 125 | .lng, .lat{ 126 | white-space: nowrap; 127 | font-size: 10px; 128 | height: math.div(( $height-btn - 2 ), 2); 129 | line-height: math.div(( $height-btn - 2 ), 2); 130 | } 131 | &:active{ 132 | transform: translateY(1px); 133 | color: $color-main; 134 | } 135 | } 136 | .operation{ 137 | display: flex; 138 | } 139 | .move{ 140 | display: flex; 141 | flex-flow: column nowrap; 142 | flex-shrink: 0; 143 | > *{ 144 | margin: 0 auto; 145 | cursor: pointer; 146 | text-align: center; 147 | font-size: 0.5rem; 148 | display: block; 149 | height: math.div(($height-btn - 2), 2); 150 | width: math.div(($height-btn - 2), 2) + 6; 151 | line-height: math.div(($height-btn - 2), 2); 152 | background-color: $border-normal; 153 | 154 | &:hover{ 155 | color: white; 156 | background: $color-main; 157 | } 158 | &:active{ 159 | transform: translateY(2px); 160 | } 161 | } 162 | .up{ 163 | @include border-radius(3px 3px 0 0); 164 | } 165 | .down{ 166 | @include border-radius(0 0 3px 3px); 167 | } 168 | } 169 | .delete{ 170 | display: flex; 171 | justify-content: center; 172 | align-items: center; 173 | i { 174 | height: $height-btn; 175 | width: $height-btn; 176 | @include border-radius(3px); 177 | cursor: pointer; 178 | &:hover{ 179 | color: white; 180 | background-color: $color-danger; 181 | } 182 | &:active{ 183 | transform: translateY(2px); 184 | } 185 | } 186 | } 187 | 188 | .hidden{ 189 | display: none; 190 | } 191 | 192 | 193 | @import "overwrite-el-ui"; 194 | @import "overwrite-amap"; 195 | -------------------------------------------------------------------------------- /src/page/info/carPlate/CarPlate.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 132 | 133 | 141 | -------------------------------------------------------------------------------- /src/page/other/About.vue: -------------------------------------------------------------------------------- 1 | 18 | 77 | 78 | 170 | -------------------------------------------------------------------------------- /src/scss/_marker.scss: -------------------------------------------------------------------------------- 1 | .amap-marker{ 2 | &:hover{ 3 | z-index: 9999 !important; 4 | } 5 | &-content{ 6 | white-space: normal !important; 7 | } 8 | } 9 | 10 | .marker{ 11 | @extend .unselectable; 12 | background-color: $bg-light; 13 | @include border-radius(10px); 14 | line-height: 1.3; 15 | color: $text-main; 16 | @include transition(all 0.3s); 17 | @include box-shadow(2px 2px 5px rgba(121, 85, 72, 0.39)); 18 | display: flex; 19 | align-items: stretch; 20 | justify-content: flex-start; 21 | flex-flow: column nowrap; 22 | &.no-content{ // 没有内容的用,没有 padding 23 | @include border-radius(10px); 24 | overflow: hidden; 25 | .marker-index{ 26 | border-bottom: none; 27 | } 28 | .marker-content{ 29 | padding: 0; 30 | } 31 | &:hover{ 32 | overflow: hidden; 33 | } 34 | } 35 | &:hover{ 36 | z-index: 99999; 37 | @include border-radius(10px); 38 | @include transition(all 0.3s); 39 | transform: scale(1.2); 40 | transform-origin: center center; 41 | .marker-index{ 42 | .index{ 43 | color: $color-main; 44 | text-shadow: 1px 2px 1px transparentize($color-main, 0.6); 45 | } 46 | } 47 | .marker-content{ 48 | 49 | } 50 | } 51 | .marker-index{ 52 | @include border-radius(10px 10px 0 0 ); 53 | border-bottom: 1px solid $border-normal; 54 | background-color: white; 55 | padding: 5px 10px; 56 | display: flex; 57 | flex-flow: row nowrap; 58 | align-items: center; 59 | justify-content: flex-start; 60 | .index{ 61 | margin-right: 10px; 62 | font-weight: bold; 63 | color: transparentize(black, 0.6); 64 | font-size: 18px; 65 | } 66 | .title{ 67 | white-space: nowrap; 68 | font-weight: bold; 69 | font-size: 15px; 70 | } 71 | } 72 | .marker-content{ 73 | padding: 4px 8px 10px; 74 | .note{ 75 | white-space: nowrap; 76 | font-size: 0.7rem; 77 | color: $text-subtitle; 78 | } 79 | .view{ 80 | @include transition(all, 0.3s); 81 | margin-top: 5px; 82 | width: 60px; 83 | @include border-radius($radius); 84 | overflow: hidden; 85 | img{ 86 | display: block; 87 | width: 100%; 88 | } 89 | &:hover{ 90 | @include box-shadow(3px 3px 5px transparentize(black, 0.7)); 91 | transform: scale(5); 92 | transform-origin: center center; 93 | margin-top: 10px; 94 | @include transition(all, 0.3s); 95 | } 96 | } 97 | } 98 | 99 | $marker-types: ( 100 | "blue": $blue, 101 | "green": $green, 102 | "red": $red, 103 | "orange": $orange, 104 | "yellow": $yellow, 105 | ); 106 | 107 | 108 | @each $name, $color in $marker-types { 109 | &.#{$name} { 110 | .marker-index{ 111 | color: white; 112 | background-color: $color; 113 | border-bottom: 1px solid $color; 114 | text-shadow: 1px 1px 1px transparentize(black, 0.9); 115 | .index{ 116 | margin-right: 10px; 117 | font-weight: bold; 118 | color: transparentize(white, 0.2); 119 | font-size: 18px; 120 | } 121 | } 122 | .marker-content{ 123 | @include border-radius(0 0 10px 10px); 124 | background-color: lighten($color, 45%); 125 | .note{ 126 | color: $text-main; 127 | } 128 | } 129 | &:hover{ 130 | .marker-index { 131 | .index{ 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | 140 | .marker-plate{ 141 | background-color: white; 142 | @include border-radius(4px); 143 | padding: 6px; 144 | line-height: 1.3; 145 | color: $text-main; 146 | @include box-shadow(2px 2px 5px rgba(121, 85, 72, 0.39)); 147 | .title{ 148 | font-family: "Times"; 149 | text-align: center; 150 | white-space: nowrap; 151 | font-weight: bold; 152 | font-size: 1.2rem; 153 | } 154 | .note{ 155 | white-space: nowrap; 156 | font-size: 0.8rem; 157 | color: $text-subtitle; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/utility.ts: -------------------------------------------------------------------------------- 1 | const AUTHORIZATION_NAME = 'Authorization' // 存储用户信息的 localStorage name,跟 Diary 通用 2 | 3 | interface EntityAuthorization { 4 | nickname: string, 5 | uid: number, 6 | email: string, 7 | phone: string, 8 | avatar: string, 9 | token: string, 10 | group_id: number, 11 | city: string, 12 | geolocation: string 13 | } 14 | 15 | // 设置 authorization 16 | function setAuthorization(nickname: string, uid: number, email: string, phone: string, avatar: string, token: string, group_id: number, city: string, geolocation: string) { 17 | localStorage.setItem(AUTHORIZATION_NAME, JSON.stringify({ 18 | nickname, uid, email, phone, avatar, token, group_id, city, geolocation 19 | })) 20 | } 21 | 22 | // 获取 authorization 23 | function getAuthorization(): EntityAuthorization { 24 | return JSON.parse(localStorage.getItem(AUTHORIZATION_NAME)) 25 | } 26 | 27 | // 删除 authorization 28 | function deleteAuthorization() { 29 | localStorage.removeItem(AUTHORIZATION_NAME) 30 | } 31 | 32 | 33 | function downloadBase64File(fileName: string, data: string) { // 下载 base64 图片 34 | let aLink = document.createElement('a') 35 | let blob = new Blob([data], { type: 'application/octet-stream' }); // Set MIME type 36 | let evt = document.createEvent("HTMLEvents") 37 | evt.initEvent("click", true, true); //initEvent 不加后两个参数在FF下会报错 事件类型,是否冒泡,是否阻止浏览器的默认行为 38 | aLink.download = fileName 39 | aLink.href = URL.createObjectURL(blob) 40 | aLink.click() 41 | } 42 | 43 | 44 | // CONST 45 | enum EnumWeekDay { 46 | '周日' = 0, 47 | '周一', 48 | '周二', 49 | '周三', 50 | '周四', 51 | '周五', 52 | '周六', 53 | } 54 | 55 | enum EnumWeekDayShort { 56 | '日' = 0, 57 | '一', 58 | '二', 59 | '三', 60 | '四', 61 | '五', 62 | '六', 63 | } 64 | 65 | // 格式化时间,输出字符串 66 | function dateFormatter(date: Date, formatString: string = 'yyyy-MM-dd hh:mm:ss') { 67 | let dateRegArray: Object = { 68 | "M+": date.getMonth() + 1, // 月份 69 | "d+": date.getDate(), // 日 70 | "h+": date.getHours(), // 小时 71 | "m+": date.getMinutes(), // 分 72 | "s+": date.getSeconds(), // 秒 73 | "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 74 | "S": date.getMilliseconds() // 毫秒 75 | } 76 | if (/(y+)/.test(formatString)) { 77 | formatString = formatString.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)) 78 | } 79 | for (let section in dateRegArray) { 80 | if (new RegExp("(" + section + ")").test(formatString)) { 81 | formatString = formatString.replace(RegExp.$1, (RegExp.$1.length === 1) ? (dateRegArray[section]) : (("00" + dateRegArray[section]).substr(("" + dateRegArray[section]).length))) 82 | } 83 | } 84 | return formatString 85 | } 86 | 87 | interface DateUtilityObject { 88 | year: number, 89 | day: number, 90 | month: number, 91 | weekday: string, 92 | weekShort: string, 93 | dateShort: string, 94 | date: string, 95 | dateFull: string, 96 | dateFullSlash: string, 97 | timeLabel: string, 98 | time: string 99 | } 100 | 101 | function dateProcess(dateString: string): DateUtilityObject { 102 | let date = new Date(dateString) 103 | let year = date.getFullYear() 104 | let month = date.getMonth() + 1 105 | let day = date.getDate() 106 | let hour = date.getHours() 107 | let minutes = date.getMinutes() 108 | // let seconds = date.getSeconds() 109 | let week = date.getDay() 110 | let timeLabel = '' 111 | if (hour >= 23 && hour < 24 || hour <= 3 && hour >= 0) { 112 | timeLabel = '深夜' 113 | } else if (hour >= 19 && hour < 23) { 114 | timeLabel = '晚上' 115 | } else if (hour >= 14 && hour < 19) { 116 | timeLabel = '傍晚' 117 | } else if (hour >= 11 && hour < 14) { 118 | timeLabel = '中午' 119 | } else if (hour >= 6 && hour < 11) { 120 | timeLabel = '早上' 121 | } else if (hour >= 3 && hour < 6) { 122 | timeLabel = '凌晨' 123 | } 124 | 125 | return { 126 | year, 127 | day, 128 | month, 129 | weekday: EnumWeekDay[week], 130 | weekShort: EnumWeekDayShort[week], 131 | dateShort: `${padNumberWith0(month)}-${padNumberWith0(day)}`, 132 | date: `${padNumberWith0(month)}月${padNumberWith0(day)}日`, 133 | dateFull: `${year}年${month}月${day}日`, 134 | dateFullSlash: `${year}/${month}/${day}`, 135 | timeLabel: timeLabel, 136 | time: `${padNumberWith0(hour)}:${padNumberWith0(minutes)}` 137 | } 138 | } 139 | 140 | function padNumberWith0(num: number) { 141 | return String(num).padStart(2, '0') 142 | } 143 | 144 | 145 | export { 146 | getAuthorization, 147 | setAuthorization, 148 | dateProcess, 149 | dateFormatter, 150 | deleteAuthorization, 151 | downloadBase64File, 152 | 153 | type EntityAuthorization 154 | } 155 | -------------------------------------------------------------------------------- /src/page/Login.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 119 | 120 | 172 | -------------------------------------------------------------------------------- /src/page/info/car/CarDepartment.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 118 | 119 | 125 | -------------------------------------------------------------------------------- /src/layout/DetailPanel.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 93 | 94 | 218 | -------------------------------------------------------------------------------- /src/page/index/LocaIndex.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 154 | 155 | 161 | -------------------------------------------------------------------------------- /src/page/tool/circle/components/CirclePanel.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 143 | 144 | 203 | -------------------------------------------------------------------------------- /src/page/Register.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 154 | 155 | 192 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory, RouteRecordRaw} from "vue-router"; 2 | import Layout from "./layout/Layout.vue" 3 | import Logout from "./page/Logout.vue" 4 | import Login from "./page/Login.vue" 5 | import Register from "./page/Register.vue" 6 | 7 | const FIXED_ROUTES: Array = [ 8 | { 9 | name: 'Root', 10 | path: '/', 11 | meta: {title: '主页', showInMenu: true, icon: 'House'}, 12 | component: Layout, 13 | redirect: '/index', 14 | children: [ 15 | {name: 'Index', path: 'index', meta: {title: '主页', showInMenu: false, icon: 'el-icon-wind-power'}, component: () => import('./page/index/Index.vue')}, 16 | ] 17 | }, 18 | { 19 | name: 'Route', 20 | path: '/route', 21 | component: Layout, 22 | meta: {title: '路线规划', showInMenu: true, icon: 'WindPower' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, 23 | children: [ 24 | {name: 'RouteLine', path: 'route-line', meta: {title: '展示', showInMenu: true, icon: 'el-icon-wind-power'}, component: () => import('./page/route/RouteLine.vue')}, 25 | {name: 'RouteList', path: 'route-list', meta: {title: '列表', showInMenu: true, icon: 'el-icon-wind-power'}, component: () => import('./page/route/RouteList.vue')}, 26 | {name: 'RouteEditor', path: 'route-editor', meta: {title: '新建 | 编辑', showInMenu: true, icon: 'el-icon-wind-power'}, component: () => import('./page/route/RouteEditor.vue')} 27 | ] 28 | }, 29 | { 30 | name: 'Pointer', 31 | path: '/pointer', 32 | component: Layout, 33 | meta: {title: '地域信息', showInMenu: true, icon: 'LocationInformation' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, 34 | children: [ 35 | {name: 'PointerViewer', path: 'pointer-viewer', meta: {title: '展示', showInMenu: true, icon: 'el-icon-wind-power'}, component: () => import('./page/pointer/PointerViewer.vue')}, 36 | {name: 'PointerList', path: 'pointer-list', meta: {title: '列表', showInMenu: true, icon: 'el-icon-wind-power'}, component: () => import('./page/pointer/PointerList.vue')}, 37 | {name: 'PointerEditor', path: 'pointer-editor', meta: {title: '新建 | 编辑', showInMenu: true, icon: 'el-icon-wind-power'}, component: () => import('./page/pointer/PointerEditor.vue')} 38 | ] 39 | }, 40 | { 41 | name: 'GPX', 42 | path: '/gpx', 43 | component: Layout, 44 | meta: {title: 'GPX', showInMenu: true, icon: 'Promotion' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, 45 | children: [ 46 | {name: 'GpxViewer', path: 'gpx-viewer', meta: {title: 'GPX 路径', showInMenu: true, icon: 'el-icon-s-promotion'}, component: () => import('./page/gpx/GpxViewer.vue')}, 47 | {name: 'GpxViewer3D', path: 'gpx-viewer-3D', meta: {title: 'GPX 路径 3D', showInMenu: true, icon: 'el-icon-s-promotion'}, component: () => import('./page/gpx/GpxViewer3D.vue')}, 48 | ] 49 | }, 50 | { 51 | name: 'Debug', 52 | path: '/debug', 53 | component: Layout, 54 | redirect: '/debug/debug', 55 | meta: {title: '调试', showInMenu: false, icon: 'el-icon-position' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, 56 | children: [ 57 | {name: 'debugIndex', path: 'debug', meta: {title: '调试', showInMenu: true, icon: 'el-icon-position' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, component: () => import('./page/debug/debug.vue')} 58 | ] 59 | }, 60 | { 61 | name: 'Tool', 62 | path: '/tool', 63 | component: Layout, 64 | redirect: '/tool/circle', 65 | meta: {title: '地图工具', showInMenu: true, icon: 'Coordinate' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, 66 | children: [ 67 | {name: 'ToolCircle' , path: 'circle' , meta: {title: '范围标记' , showInMenu: true}, component: () => import('./page/tool/circle/ToolCircle.vue')}, 68 | {name: 'DistrictInfo', path: 'district-info', meta: {title: '城市各区县', showInMenu: true}, component: () => import('./page/info/area/District.vue')}, 69 | ] 70 | }, 71 | { 72 | name: 'Info', 73 | path: '/info', 74 | component: Layout, 75 | redirect: '/info/plate', 76 | meta: {title: '济南本地信息', showInMenu: true, icon: 'CreditCard' /* 菜单 icon 对应 Element UI 中的 ICON class 名 */}, 77 | children: [ 78 | {name: 'MotorHighway' , path: 'motor-highway' , meta: {title: '摩托车高速' , showInMenu: true} , component: () => import('./page/info/motorHighway/MotorHighway.vue')} , 79 | {name: 'InfoPlate' , path: 'plate' , meta: {title: '山东各市车牌' , showInMenu: true} , component: () => import('./page/info/carPlate/CarPlate.vue')} , 80 | {name: 'InfoCarDepartment' , path: 'car-department' , meta: {title: '济南车管所' , showInMenu: true} , component: () => import('./page/info/car/CarDepartment.vue')} , 81 | {name: 'HighwayXueye' , path: 'highway-xueye' , meta: {title: '济南籍车辆高速免费' , showInMenu: true} , component: () => import('./page/info/highwayXueye/HighwayXueye.vue')} , 82 | ] 83 | }, 84 | 85 | { 86 | name: 'Other', 87 | path: '/other', 88 | component: Layout, 89 | redirect: '/other/map-loca', 90 | meta: {title: '其它', showInMenu: true, icon: 'Grape'}, 91 | children: [ 92 | {name: 'MapLoca', path: 'map-loca', meta: {title: '脉冲图', showInMenu: true}, component: () => import('./page/other/map/MapLoca.vue')}, 93 | ] 94 | }, 95 | { 96 | name: 'AboutFramework', 97 | path: '/about', 98 | component: Layout, 99 | redirect: '/about/about', 100 | meta: {title: '关于', showInMenu: true, icon: 'ChatDotRound'}, 101 | children: [ 102 | {name: 'About', path: 'about', meta: {isAdmin: false, title: '关于', showInMenu: false, icon: 'el-icon-warning-outline',}, component: () => import('./page/other/About.vue')}, 103 | ] 104 | }, 105 | { 106 | name: 'Logout', path: '/logout', 107 | meta: {title: '退出登录', showInMenu: false, icon: 'BottomRight',}, 108 | component: Logout, 109 | }, 110 | { 111 | name: 'Login', path: '/login', 112 | meta: {title: '登录', showInMenu: false, icon: 'el-icon-user-solid',}, 113 | component: Login, 114 | 115 | }, 116 | { 117 | name: 'Register', path: '/register', 118 | meta: {title: '注册', showInMenu: false, icon: 'el-icon-user-solid',}, 119 | component: Register, 120 | }, 121 | // { 122 | // name: 'NoPage', path: '*', 123 | // meta: {title: '404', showInMenu: false, icon: 'el-icon-user-solid',}, 124 | // component: Login, 125 | // } 126 | ] 127 | 128 | 129 | const router = createRouter({ 130 | history: createWebHashHistory(), 131 | routes: FIXED_ROUTES 132 | }) 133 | 134 | 135 | export { 136 | router, 137 | FIXED_ROUTES, // 需要显示在边栏的菜单 138 | } 139 | -------------------------------------------------------------------------------- /src/layout/Navbar.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 119 | 120 | 227 | -------------------------------------------------------------------------------- /src/page/info/motorHighway/MotorHighway.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 162 | 163 | 171 | -------------------------------------------------------------------------------- /src/page/route/components/RouteLineListPanel.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 152 | 153 | 234 | -------------------------------------------------------------------------------- /src/scss/animate.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --animate-duration: 1s; 3 | --animate-delay: 1s; 4 | --animate-repeat: 1; 5 | } 6 | .animate__animated { 7 | -webkit-animation-duration: 1s; 8 | animation-duration: 1s; 9 | -webkit-animation-duration: var(--animate-duration); 10 | animation-duration: var(--animate-duration); 11 | -webkit-animation-fill-mode: both; 12 | animation-fill-mode: both; 13 | } 14 | /* Fading exits */ 15 | @-webkit-keyframes fadeOut { 16 | from { 17 | opacity: 1; 18 | } 19 | 20 | to { 21 | opacity: 0; 22 | } 23 | } 24 | @keyframes fadeOut { 25 | from { 26 | opacity: 1; 27 | } 28 | 29 | to { 30 | opacity: 0; 31 | } 32 | } 33 | .animate__fadeOut { 34 | -webkit-animation-name: fadeOut; 35 | animation-name: fadeOut; 36 | } 37 | @-webkit-keyframes fadeOutDown { 38 | from { 39 | opacity: 1; 40 | } 41 | 42 | to { 43 | opacity: 0; 44 | -webkit-transform: translate3d(0, 100%, 0); 45 | transform: translate3d(0, 100%, 0); 46 | } 47 | } 48 | @keyframes fadeOutDown { 49 | from { 50 | opacity: 1; 51 | } 52 | 53 | to { 54 | opacity: 0; 55 | -webkit-transform: translate3d(0, 100%, 0); 56 | transform: translate3d(0, 100%, 0); 57 | } 58 | } 59 | .animate__fadeOutDown { 60 | -webkit-animation-name: fadeOutDown; 61 | animation-name: fadeOutDown; 62 | } 63 | @-webkit-keyframes fadeOutDownBig { 64 | from { 65 | opacity: 1; 66 | } 67 | 68 | to { 69 | opacity: 0; 70 | -webkit-transform: translate3d(0, 2000px, 0); 71 | transform: translate3d(0, 2000px, 0); 72 | } 73 | } 74 | @keyframes fadeOutDownBig { 75 | from { 76 | opacity: 1; 77 | } 78 | 79 | to { 80 | opacity: 0; 81 | -webkit-transform: translate3d(0, 2000px, 0); 82 | transform: translate3d(0, 2000px, 0); 83 | } 84 | } 85 | 86 | .animate__animated.animate__faster { 87 | -webkit-animation-duration: calc(1s / 2); 88 | animation-duration: calc(1s / 2); 89 | -webkit-animation-duration: calc(var(--animate-duration) / 2); 90 | animation-duration: calc(var(--animate-duration) / 2); 91 | } 92 | .animate__animated.animate__fast { 93 | -webkit-animation-duration: calc(1s * 0.8); 94 | animation-duration: calc(1s * 0.8); 95 | -webkit-animation-duration: calc(var(--animate-duration) * 0.8); 96 | animation-duration: calc(var(--animate-duration) * 0.8); 97 | } 98 | 99 | @-webkit-keyframes bounceInDown { 100 | from, 101 | 60%, 102 | 75%, 103 | 90%, 104 | to { 105 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 106 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 107 | } 108 | 109 | 0% { 110 | opacity: 0; 111 | -webkit-transform: translate3d(0, -3000px, 0) scaleY(3); 112 | transform: translate3d(0, -3000px, 0) scaleY(3); 113 | } 114 | 115 | 60% { 116 | opacity: 1; 117 | -webkit-transform: translate3d(0, 25px, 0) scaleY(0.9); 118 | transform: translate3d(0, 25px, 0) scaleY(0.9); 119 | } 120 | 121 | 75% { 122 | -webkit-transform: translate3d(0, -10px, 0) scaleY(0.95); 123 | transform: translate3d(0, -10px, 0) scaleY(0.95); 124 | } 125 | 126 | 90% { 127 | -webkit-transform: translate3d(0, 5px, 0) scaleY(0.985); 128 | transform: translate3d(0, 5px, 0) scaleY(0.985); 129 | } 130 | 131 | to { 132 | -webkit-transform: translate3d(0, 0, 0); 133 | transform: translate3d(0, 0, 0); 134 | } 135 | } 136 | @keyframes bounceInDown { 137 | from, 138 | 60%, 139 | 75%, 140 | 90%, 141 | to { 142 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 143 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 144 | } 145 | 146 | 0% { 147 | opacity: 0; 148 | -webkit-transform: translate3d(0, -3000px, 0) scaleY(3); 149 | transform: translate3d(0, -3000px, 0) scaleY(3); 150 | } 151 | 152 | 60% { 153 | opacity: 1; 154 | -webkit-transform: translate3d(0, 25px, 0) scaleY(0.9); 155 | transform: translate3d(0, 25px, 0) scaleY(0.9); 156 | } 157 | 158 | 75% { 159 | -webkit-transform: translate3d(0, -10px, 0) scaleY(0.95); 160 | transform: translate3d(0, -10px, 0) scaleY(0.95); 161 | } 162 | 163 | 90% { 164 | -webkit-transform: translate3d(0, 5px, 0) scaleY(0.985); 165 | transform: translate3d(0, 5px, 0) scaleY(0.985); 166 | } 167 | 168 | to { 169 | -webkit-transform: translate3d(0, 0, 0); 170 | transform: translate3d(0, 0, 0); 171 | } 172 | } 173 | .animate__bounceInDown { 174 | -webkit-animation-name: bounceInDown; 175 | animation-name: bounceInDown; 176 | } 177 | 178 | 179 | @-webkit-keyframes slideInLeft { 180 | from { 181 | -webkit-transform: translate3d(-100%, 0, 0); 182 | transform: translate3d(-100%, 0, 0); 183 | visibility: visible; 184 | } 185 | 186 | to { 187 | -webkit-transform: translate3d(0, 0, 0); 188 | transform: translate3d(0, 0, 0); 189 | } 190 | } 191 | @keyframes slideInLeft { 192 | from { 193 | -webkit-transform: translate3d(-100%, 0, 0); 194 | transform: translate3d(-100%, 0, 0); 195 | visibility: visible; 196 | } 197 | 198 | to { 199 | -webkit-transform: translate3d(0, 0, 0); 200 | transform: translate3d(0, 0, 0); 201 | } 202 | } 203 | .animate__slideInLeft { 204 | -webkit-animation-name: slideInLeft; 205 | animation-name: slideInLeft; 206 | } 207 | @-webkit-keyframes slideInRight { 208 | from { 209 | -webkit-transform: translate3d(100%, 0, 0); 210 | transform: translate3d(100%, 0, 0); 211 | visibility: visible; 212 | } 213 | 214 | to { 215 | -webkit-transform: translate3d(0, 0, 0); 216 | transform: translate3d(0, 0, 0); 217 | } 218 | } 219 | @keyframes slideInRight { 220 | from { 221 | -webkit-transform: translate3d(100%, 0, 0); 222 | transform: translate3d(100%, 0, 0); 223 | visibility: visible; 224 | } 225 | 226 | to { 227 | -webkit-transform: translate3d(0, 0, 0); 228 | transform: translate3d(0, 0, 0); 229 | } 230 | } 231 | .animate__slideInRight { 232 | -webkit-animation-name: slideInRight; 233 | animation-name: slideInRight; 234 | } 235 | 236 | @keyframes slideOutLeft { 237 | from { 238 | -webkit-transform: translate3d(0, 0, 0); 239 | transform: translate3d(0, 0, 0); 240 | } 241 | 242 | to { 243 | visibility: hidden; 244 | -webkit-transform: translate3d(-100%, 0, 0); 245 | transform: translate3d(-100%, 0, 0); 246 | } 247 | } 248 | .animate__slideOutLeft { 249 | -webkit-animation-name: slideOutLeft; 250 | animation-name: slideOutLeft; 251 | } 252 | @-webkit-keyframes slideOutRight { 253 | from { 254 | -webkit-transform: translate3d(0, 0, 0); 255 | transform: translate3d(0, 0, 0); 256 | } 257 | 258 | to { 259 | visibility: hidden; 260 | -webkit-transform: translate3d(100%, 0, 0); 261 | transform: translate3d(100%, 0, 0); 262 | } 263 | } 264 | @keyframes slideOutRight { 265 | from { 266 | -webkit-transform: translate3d(0, 0, 0); 267 | transform: translate3d(0, 0, 0); 268 | } 269 | 270 | to { 271 | visibility: hidden; 272 | -webkit-transform: translate3d(100%, 0, 0); 273 | transform: translate3d(100%, 0, 0); 274 | } 275 | } 276 | .animate__slideOutRight { 277 | -webkit-animation-name: slideOutRight; 278 | animation-name: slideOutRight; 279 | } 280 | 281 | @-webkit-keyframes bounceOutUp { 282 | 20% { 283 | -webkit-transform: translate3d(0, -10px, 0) scaleY(0.985); 284 | transform: translate3d(0, -10px, 0) scaleY(0.985); 285 | } 286 | 287 | 40%, 288 | 45% { 289 | opacity: 1; 290 | -webkit-transform: translate3d(0, 20px, 0) scaleY(0.9); 291 | transform: translate3d(0, 20px, 0) scaleY(0.9); 292 | } 293 | 294 | to { 295 | opacity: 0; 296 | -webkit-transform: translate3d(0, -2000px, 0) scaleY(3); 297 | transform: translate3d(0, -2000px, 0) scaleY(3); 298 | } 299 | } 300 | @keyframes bounceOutUp { 301 | 20% { 302 | -webkit-transform: translate3d(0, -10px, 0) scaleY(0.985); 303 | transform: translate3d(0, -10px, 0) scaleY(0.985); 304 | } 305 | 306 | 40%, 307 | 45% { 308 | opacity: 1; 309 | -webkit-transform: translate3d(0, 20px, 0) scaleY(0.9); 310 | transform: translate3d(0, 20px, 0) scaleY(0.9); 311 | } 312 | 313 | to { 314 | opacity: 0; 315 | -webkit-transform: translate3d(0, -2000px, 0) scaleY(3); 316 | transform: translate3d(0, -2000px, 0) scaleY(3); 317 | } 318 | } 319 | .animate__bounceOutUp { 320 | -webkit-animation-name: bounceOutUp; 321 | animation-name: bounceOutUp; 322 | } -------------------------------------------------------------------------------- /src/page/tool/circle/ToolCircle.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 266 | 267 | 288 | -------------------------------------------------------------------------------- /src/page/debug/debug.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 315 | 316 | 323 | -------------------------------------------------------------------------------- /src/page/pointer/PointerViewer.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 378 | 379 | 403 | -------------------------------------------------------------------------------- /src/page/other/map/MapLoca.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 355 | 356 | 360 | --------------------------------------------------------------------------------