提交 6736b9be 作者: 方治民

合并分支 'dev_fzm' 到 'dev'

Dev fzm

查看合并请求 !53
# APP 名称
VITE_GLOB_APP_NAME = Basic APP
VITE_GLOB_APP_NAME = Hunan Weather App
# APP 描述
VITE_GLOB_APP_DESCRIPTION = APP 基础工程
VITE_GLOB_APP_DESCRIPTION = 湖南天气 APP
# API 接口地址
VITE_GLOB_API_URL=http://192.168.0.156:8081
VITE_GLOB_API_URL=http://qks.straw.yiring.com:40051/tbm
# API 接口地址前缀
VITE_GLOB_API_URL_PREFIX=/api
# API 接口地址
VITE_GLOB_API_URL=http://localhost:8080
VITE_GLOB_API_URL=http://qks.straw.yiring.com:40051/tbm
# API 接口地址前缀
VITE_GLOB_API_URL_PREFIX=/api
# APP 基础工程 (uni-app-vue3)
# 湖南天气 APP 五期
> 项目 Fork 自 [uni-app-vite-ts](https://gitee.com/dcloud/uni-preset-vue/blob/vite-ts)
> 项目 Fork 自 [basic-uniapp-v3](https://gitlab.yiring.com/basic/basic-uniapp-v3)
> **注意事项**: 运行/调试/编译/发布环境使用 `HBuilderX`, 开发工具统一使用 `Visual Studio Code`
## 开发环境
......@@ -22,15 +22,4 @@
## TODO
<!-- prettier-ignore -->
- [x] 项目构建,文档编写
- [x] [commitizen](https://github.com/commitizen/cz-cli), [changelog](https://www.cnblogs.com/mengfangui/p/12634845.html)
- [x] 完善网络请求相关配置
- [x] 集成 [Stomp WebSocket](https://github.com/jmesnil/stomp-websocket)
- [x] 集成 [Pont](https://github.com/alibaba/pont)
- [x] 集成 [Pinia](https://pinia.vuejs.org/)
- [x] 集成 [Vue-i18n](https://github.com/intlify/vue-i18n-next)
- [x] 集成 [UnoCSS](https://github.com/antfu/unocss)
- [x] UI 组件集成 [FirstUI](https://gitlab.yiring.com/open/first-ui), [uni-ui](https://uniapp.dcloud.net.cn/component/uniui/quickstart.html)
- [x] 接入 [消息推送](https://uniapp.dcloud.net.cn/unipush.html)
- [x] 接入 [APP 升级中心](https://uniapp.dcloud.net.cn/uniCloud/upgrade-center.html)
- [ ] 适配上传/下载接口的自动化生成模板(包装 uni.uploadFile 与 uni.downloadFile 方法实现)
> 项目开发中, 以下内容待完善
......@@ -11,7 +11,7 @@ import tags from '@dcloudio/uni-helper-json/dist/tags.json'
/**
* 根据 @dcloudio/uni-helper-json 生成组件 .d.ts 描述文件,对组件进行高亮展示
*/
function generateUniComponentsTypes() {
async function generateUniComponentsTypes() {
const components = Object.keys(tags).map((key) => `'${key}': UniComponent`)
const content = `
// generated by @dcloudio/uni-helper-json/dist/tags.json
......@@ -32,7 +32,7 @@ function generateUniComponentsTypes() {
export {}
`
const formatted = prettier.format(content, {
const formatted = await prettier.format(content, {
parser: 'typescript',
// eslint-disable-next-line @typescript-eslint/no-require-imports
...require('../../prettier.config'),
......
......@@ -47,6 +47,7 @@ module.exports = {
'workflow',
'types',
'release',
'upd',
],
],
},
......
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="stylesheet" type="text/css" href="https://at.alicdn.com/t/c/font_4105118_8gdcfjv45sh.css" /> -->
<script>
const coverSupport =
'CSS' in window &&
......
{
"name": "basic-app",
"version": "3.0.3.0",
"description": "APP 基础工程",
"name": "hntq-app",
"version": "5.0.0",
"description": "湖南天气 APP v5",
"keywords": [
"app",
"basic"
"basic",
"hntq"
],
"bugs": {
"url": "https://gitlab.yiring.com/basic/basic-uniapp-v3/issues"
"url": "https://gitlab.yiring.com/hntq/hntq-v5/issues"
},
"repository": {
"type": "git",
"url": "https://gitlab.yiring.com/basic/basic-uniapp-v3.git"
"url": "https://gitlab.yiring.com/hntq/hntq-v5.git"
},
"author": {
"name": "yiring",
......@@ -66,29 +67,34 @@
}
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-components": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-h5": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-alipay": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-baidu": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-jd": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-lark": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-qq": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-toutiao": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-app": "3.0.0-3081220230817001",
"@dcloudio/uni-app-plus": "3.0.0-3081220230817001",
"@dcloudio/uni-components": "3.0.0-3081220230817001",
"@dcloudio/uni-h5": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-alipay": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-baidu": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-jd": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-lark": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-qq": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-toutiao": "3.0.0-3081220230817001",
"@dcloudio/uni-mp-weixin": "3.0.0-3081220230817001",
"@dcloudio/uni-quickapp-webview": "3.0.0-3081220230817001",
"@dcloudio/uni-ui": "^1.4.28",
"@faker-js/faker": "^8.0.2",
"@turf/turf": "^6.5.0",
"@types/mapbox-gl": "^2.7.13",
"@vue/runtime-core": "~3.2.47",
"@vueuse/core": "^10.2.1",
"@vueuse/core": "^10.3.0",
"axios": "^1.4.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.9",
"lodash-es": "^4.17.21",
"mapbox-gl": "^2.15.0",
"mapbox-gl-controls": "^2.3.5",
"nanoid": "^4.0.2",
"pinia": "~2.0.36",
"pnpm": "8.6.7",
"qs": "~6.9.7",
"stompjs": "^2.3.3",
"urijs": "^1.19.11",
......@@ -98,62 +104,62 @@
"vue-types": "^5.1.1"
},
"devDependencies": {
"@antfu/eslint-config": "^0.39.8",
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@antfu/eslint-config": "^0.40.2",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@dcloudio/types": "^3.3.3",
"@dcloudio/uni-automator": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-3081120230719001",
"@dcloudio/uni-automator": "3.0.0-3081220230817001",
"@dcloudio/uni-cli-shared": "3.0.0-3081220230817001",
"@dcloudio/uni-helper-json": "^1.0.13",
"@dcloudio/uni-stacktracey": "3.0.0-alpha-3081120230719001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-3081120230719001",
"@iconify/json": "^2.2.95",
"@dcloudio/uni-stacktracey": "3.0.0-3081220230817001",
"@dcloudio/vite-plugin-uni": "3.0.0-3081220230817001",
"@iconify/json": "^2.2.102",
"@types/crypto-js": "^4.1.1",
"@types/lodash-es": "^4.17.8",
"@types/node": "^18.17.1",
"@types/prettier": "^2.7.3",
"@types/node": "^20.5.0",
"@types/qs": "^6.9.7",
"@types/stompjs": "^2.3.5",
"@types/urijs": "^1.19.19",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"commitizen": "^4.3.0",
"conventional-changelog-cli": "^3.0.0",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^7.0.0",
"cz-git": "^1.7.0",
"czg": "^1.7.0",
"cz-git": "^1.7.1",
"czg": "^1.7.1",
"dotenv": "^16.3.1",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.15.1",
"eslint": "^8.47.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"husky": "^8.0.3",
"jest": "27.0.4",
"jest-environment-node": "27.5.1",
"less": "^4.1.3",
"lint-staged": "^13.2.3",
"less": "^4.2.0",
"lint-staged": "^14.0.0",
"npm-run-all": "^4.1.5",
"picocolors": "^1.0.0",
"pont-engine": "^1.5.11",
"postcss": "^8.4.27",
"pont-engine": "^1.5.12",
"postcss": "^8.4.28",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^2.8.8",
"prettier": "^3.0.2",
"rimraf": "^5.0.1",
"sass": "^1.64.1",
"sass": "^1.65.1",
"sort-package-json": "^2.5.1",
"stylelint": "^15.10.2",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"typescript": "~5.0.4",
"unocss": "^0.53.6",
"unocss-preset-weapp": "^0.53.7",
"typescript": "~5.1.6",
"unocss": "^0.54.3",
"unocss-preset-weapp": "^0.54.0",
"unplugin-auto-import": "^0.16.6",
"unplugin-transform-class": "^0.5.1",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.7",
"vite": "^4.4.9",
"vue-eslint-parser": "^9.3.1"
},
"engines": {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<script setup lang="ts">
import { checkUpgrade } from '@/utils/upgrade'
import { setAuthCache } from './utils/auth'
import { TOKEN_KEY } from '/@/enums/cacheEnum'
// import * as Push from '@/utils/push'
......@@ -14,6 +16,9 @@
// 监听在线消息
// Push.listen()
// TODO: 临时设置token
setAuthCache(TOKEN_KEY, '68b919d2-2ea0-4ad3-9521-9e624e1563ac')
})
onShow(() => {
......
import { otherHttp } from '/@/utils/http/axios'
// import { ContentTypeEnum } from '/@/enums/httpEnum'
enum monitorApi {
getWindList = '/live/wind/getWindList', // 查询大风数据
}
/**
* @description: 查询大风数据
*/
export function getWindList(params) {
return otherHttp.get({
url: monitorApi.getWindList,
params,
})
}
/**
* @desc 新增
*/
import * as defs from '../../baseClass'
import { defHttp } from '/@/utils/http/axios'
export class Params {
/** 组件 */
component?: string
/** 是否启用 */
enable?: boolean
/** 是否隐藏 */
hidden?: boolean
/** 图标 */
icon?: string
/** 元数据 */
meta?: string
/** 名称 */
name: string
/** 路径 */
path?: string
/** 父级ID */
pid?: string
/** 重定向 */
redirect?: string
/** 序号 */
serial?: number
/** 权限类型 */
type: 'DIR' | 'MENU' | 'BUTTON'
/** 标识 */
uid: string
}
export const init = new defs.Result()
export function request(params: Params, config?: http.RequestConfig<Params>, options?: http.RequestOptions) {
return defHttp.request(
{
url: '/sys/permission/add',
method: 'POST',
data: params,
...config,
},
options,
)
}
......@@ -82,11 +82,6 @@ export enum Status {
INTERNAL_SERVER_ERROR = 500,
/**
* 未知错误
*/
UNKNOWN_ERROR = 500,
/**
* API 未实现
*/
NOT_IMPLEMENTED = 501,
......
.example-page {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
justify-content: center;
}
.box-shadow-def {
box-shadow: 0rpx 0rpx 12rpx 0rpx rgb(3 21 37 / 15%);
}
.box-shadow-bottom {
background: #fff;
box-shadow: 0 0 6px 0 rgb(3 21 37 / 15%);
}
.translateX(-50%) {
transform: translateX(-50%);
}
.letter-spacing-1 {
letter-spacing: 1px;
}
.border-bottom {
border-bottom: 2rpx solid #eee;
}
.line-blue {
position: relative;
padding-left: 20rpx;
&::before {
content: '';
width: 6rpx;
height: 40rpx;
background: #1890ff;
border-radius: 3rpx;
position: absolute;
left: 0;
top: 8rpx;
z-index: 10;
}
}
......@@ -97,6 +97,7 @@
}
const hasError = ref(false)
function onError() {
if (hasError.value) {
return
......
<script setup lang="ts">
import dayjs from 'dayjs'
const props = defineProps({
type: {
type: String,
default: '5',
},
})
const opts = [
{ label: '小时级', value: '4', format: 'MM月DD日HH时', valueFormat: 'YYYY-MM-DD HH' },
{ label: '分钟级', value: '5', format: 'MM月DD日HH时mm分', valueFormat: 'YYYY-MM-DD HH:mm' },
]
const _type = ref<string>(props.type)
const active = computed(() => opts.find((v) => v.value === _type.value))
// 是否显示
const show = ref<boolean>(true)
// 开始时间
const sTime = ref<string>(dayjs().subtract(1, 'day').format(active.value.valueFormat))
// 结束时间
const eTime = ref<string>(dayjs().format(active.value.valueFormat))
watch(
() => props.type,
(val) => {
_type.value = val
},
)
// 时间组件改变
function change(e) {
const format = active.value.valueFormat
const { startDate, endDate } = toRaw(e)
sTime.value = dayjs(toRaw(startDate).result).format(format)
eTime.value = dayjs(toRaw(endDate).result).format(format)
}
function cancel() {
show.value = false
}
</script>
<template>
<view class="h-84 lh-10 flex text-28 bg-#fff p-2 pt-0 pb-0 pos-relative">
<fui-icon name="wait" size="40" />
<text class="ml-1">小时级:</text>
<text class="text-#1890FF" @click="show = !show">
{{ dayjs(sTime).format(active.format) }}
-
{{ dayjs(eTime).format(active.format) }}
</text>
<fui-icon name="arrowright" size="45" class="absolute right-16" color="#000000" />
<!-- 时间组件 -->
<fui-date-picker
range
:value="dayjs(sTime).format(active.valueFormat)"
:show="show"
:type="type"
@change="change"
@cancel="cancel"
/>
</view>
</template>
......@@ -1036,6 +1036,7 @@
}, 80)
},
pickerChange(e) {
if (!this.isShow) return
//1-年 2-年月 3-年月日 4-年月日 时 5-年月日 时分 6-时分 7-时分秒 8-分秒
let value = e.detail.value;
let type = Number(this.type)
......
......@@ -730,6 +730,7 @@
}, 50)
},
pickerChange(e) {
if (!this.isShow) return
let value = e.detail.value;
if (!this.isInitShow || value.length != this.layer) return;
if (this.linkage) {
......
......@@ -385,6 +385,8 @@
})
}
this.itemList = vals;
} else {
this.itemList = []
}
},
itemClick(index) {
......
......@@ -8,7 +8,7 @@
/* #ifndef APP-NVUE */
page {
/* 行为相关颜色 */
--fui-color-primary: #465CFF;
--fui-color-primary: #1890ff;
--fui-color-success: #09BE4F;
--fui-color-warning: #FFB703;
--fui-color-danger: #FF2B2B;
......
<script setup lang="ts">
import { liveModules } from '/@/utils/const/Navigation'
import type { NavItemType } from '/@/utils/const/Navigation'
defineProps({
dataSouce: {
type: Array as PropType<NavItemType[]>,
default: () => liveModules,
},
})
// const colors = {
// m: '#1890FF',
// f: '#FF9A03',
// c: '#63C56D',
// w: '#1890FF',
// }
// const getColor = (type: string) => colors[type]
function goRoute(item: NavItemType) {
if (!item?.navigate?.url) {
Message.alert('版块升级中,敬请期待!', '')
return
}
if (item.type === 'webview') {
uni.navigateTo({
url: `/pages/common/webview/index?title=${encodeURI(item.label)}&link=${encodeURIComponent(
item.navigate.url as string,
)}`,
})
} else {
uni.navigateTo(item.navigate)
}
}
const lastString = (string: string) => string.charAt(string.length - 1)
const getUrl = (item) => `/static/images/business/${lastString(item.value)}/${item.value}.svg`
</script>
<template>
<fui-grid :columns="4" class="mt-4 bg-#fff" :showBorder="false">
<fui-grid-item v-for="(item, index) in dataSouce" :key="index" :highlight="false" @click="goRoute(item)">
<view class="fui-grid__cell flex flex-col flex-center">
<!-- <image src="/static/uni-logo.png" class="fui-icon w50" mode="widthFix" /> -->
<!-- :color="getColor(lastString(item.value))" -->
<!-- <FuiIcon custom-prefix="font_family" :name="`icon-${item.value}`" size="100" :primary="true" /> -->
<image :src="getUrl(item)" class="fui-icon w-100 h-98" />
<text class="text-24 mt-2">{{ item.label }}</text>
</view>
</fui-grid-item>
</fui-grid>
</template>
<style lang="less">
.fui-grid__cell {
border: none;
}
</style>
<script setup lang="ts">
import { has } from 'lodash-es'
import { useSlots } from 'vue'
defineProps({
title: {
type: String,
default: '监测预报',
},
url: {
type: String,
default: '/static/warnforecast.png',
},
myStyle: {
type: String,
default: '',
},
bgColor: {
type: String,
default: '#1F8BF6',
},
})
const isSlot = computed(() => has(toRaw(useSlots()), 'content'))
</script>
<template>
<!-- 头部 -->
<view
:style="`background:${bgColor} url(${url}) no-repeat center bottom;background-size:100% 100%;${myStyle}`"
class="h-171 bg-[url(/static/warnforecast.png)] text-c0 lh-32 text-center text-36 font-500 pos-relative"
>
<slot name="content"></slot>
<text v-if="!isSlot"> {{ title }}</text>
</view>
</template>
<script lang="ts" setup>
defineProps({
title: {
type: String,
default: '气象服务',
},
otherTitle: {
type: String,
default: '查看全部',
},
otherTitleVis: {
type: Boolean,
default: true,
},
})
</script>
<template>
<!-- 头部 -->
<view class="flex lh-7 justify-between pb-3 b-#EEEEEE b-b-2 b-b-solid">
<view class="text-black text-35 font-600 line-blue">{{ title }}</view>
<view class="b-2 b-#3E8FFD b-solid b-rd-4 pl-3 pr-1 text-#3E8FFD flex align-center" v-if="otherTitleVis">
<text class="text-28 font-500">{{ otherTitle }}</text>
<fui-icon name="arrowright" size="36" color="#3E8FFD" />
</view>
<slot name="content"></slot>
</view>
</template>
<script setup lang="ts"></script>
<template>
<view class="pb-18">
<slot name="container"></slot>
</view>
</template>
import type mapboxgl from 'mapbox-gl'
import qs from 'qs'
import { isEmpty, isNull, isObject, isString, isUndefined, omitBy } from 'lodash-es'
import type { GeoJSONSourceDataUrlParams, MapboxConfig, MapboxInstance } from './index'
import { API_URL, API_URL_PREFIX } from '/@/utils/net'
import { isProdMode } from '@/utils/env'
import { getToken } from '@/utils/auth'
// 组件名称
export const name = 'Mapbox'
/**
* 注册 Mapbox 实例
* @param config Mapbox 配置项
*/
export function useMapbox<T extends MapboxInstance, P extends MapboxConfig>(
config: P,
): [(instance: T) => void, MapboxInstance] {
const instanceRef = ref<T>()
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
config && instance?.setConfig(config)
}
function getInstance() {
const instance = unref(instanceRef)
if (!instance) {
console.warn('Mapbox instance is undefined!')
}
return instance as T
}
return [
register,
{
setConfig: (config: Partial<P>) => getInstance()?.setConfig(config),
isReady: computed(() => instanceRef?.value?.isReady) as unknown as ComputedRef<boolean>,
on: (type: string, layerId: string, listener: (...args: any[]) => void) =>
getInstance()?.on(type, layerId, listener),
addSource: (id: string, source: mapboxgl.AnySourceData) => getInstance()?.addSource(id, source),
removeSource: (id: string) => getInstance()?.removeSource(id),
setGeoJSONSourceForRequest: (
id: string,
config: string | GeoJSONSourceDataUrlParams,
handler?: (
data: Recordable,
) => GeoJSON.Feature<GeoJSON.Geometry> | GeoJSON.FeatureCollection<GeoJSON.Geometry>,
filters?: string[],
) =>
getInstance()?.setGeoJSONSourceForRequest(id, buildGeoJSONSourceDataUrl(config), handler, [
'jsonObject',
'liveVos',
...(filters || []),
]),
setGeoJSONSourceData: (id: string, data: mapboxgl.GeoJSONSource['setData'], filter?: string) =>
getInstance()?.setGeoJSONSourceData(id, data, filter),
setVectorTileSourceTiles: (id: string, tiles: string[]) =>
getInstance()?.setVectorTileSourceTiles(id, tiles),
addLayer: (layer: mapboxgl.Layer, beforeId?: string) => getInstance()?.addLayer(layer, beforeId),
removeLayer: (id: string) => getInstance()?.removeLayer(id),
setPaintProperty: (layerId: string, name: string, value: any) =>
getInstance()?.setPaintProperty(layerId, name, value),
setLayoutProperty: (layerId: string, name: string, value: any) =>
getInstance()?.setLayoutProperty(layerId, name, value),
setFilter: (layerId: string, filter: any[]) => getInstance()?.setFilter(layerId, filter),
flyTo: (options: mapboxgl.FlyToOptions) => getInstance()?.flyTo(options),
},
]
}
export function buildGeoJSONSourceDataUrl(config: GeoJSONSourceDataUrlParams | string): string {
const baseURL = `${API_URL}${API_URL_PREFIX}`
const defaultParams = {
Authorization: getToken(),
}
const url = typeof config === 'string' ? config : config.url
const params = typeof config === 'string' ? defaultParams : { ...defaultParams, ...config.params }
const query = qs.stringify(
omitBy(
params,
(value) =>
isNull(value) ||
isUndefined(value) ||
(isString(value) && isEmpty(value)) ||
(isObject(value) && isEmpty(value)),
),
{ addQueryPrefix: true },
)
return `${baseURL}${url}${query}`
}
export type * from './src/types'
export * from './src/hook'
export { default as AffixFilterWidget } from './src/AffixFilter.vue'
/**
* 吸附过滤器组件
*
* 说明: 用于吸附在地图上的过滤器组件,可以用于筛选地图上的数据或改变查询条件等,支持的组件有: Select
*
* 可以应用到的一些模块:
* 1. 山洪地质灾害
* 2. 森林火险
* 3. 水情监测
* 4. 卫星云图
* 5. 形势场
* 6. 雷达拼图
* ...
*/
<!-- 吸附过滤器组件 -->
<script setup lang="ts">
import type { Option, OptionItem } from './types'
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 布局
layout: {
type: String,
default: 'horizontal',
},
// 风格
style: {
type: String,
default: 'button',
},
// 距离顶部的距离
top: {
type: Number,
default: 125,
},
// 距离左侧的距离
left: {
type: Number,
default: 30,
},
// 选项
options: {
type: Array as PropType<Option[]>,
default: () => [],
},
})
const emits = defineEmits(['register'])
const positionTop = computed(() => `${data.top}rpx`)
const positionLeft = computed(() => `${data.left}rpx`)
const data = reactive({
show: props.show,
layout: props.layout,
style: props.style,
top: props.top,
left: props.left,
options: props.options,
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function setProps(value: Partial<typeof props>) {
Object.assign(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
function setOptionItems(key: string, items: OptionItem[]) {
data.options = data.options.map((option) => {
if (option.key === key) {
option.items = items
}
return option
})
}
function setOptionItemChecked(key: string, value: string) {
data.options = data.options.map((option) => {
if (option.key === key) {
option.items = option.items.map((item) => {
item.checked = item.value === value
return item
})
}
return option
})
}
const zIndex = computed(() => (model.show ? 200 : 110))
const model = reactive({
show: false,
activeOption: null as Option | null,
})
function optionClick(option: Option) {
console.log(option)
model.activeOption = option
model.show = true
}
function getItemLabel(option: Option) {
return option.items.find((item) => item.checked)?.text ?? option.placeholder
}
function optionChange(e: { text: string; value: string }) {
const currentOptionChecked = model.activeOption?.items.find((item) => item.checked)
data.options = data.options.map((option) => {
if (option.key === model.activeOption?.key) {
option.items = option.items.map((item) => {
item.checked = item.value === e.value
return item
})
}
return option
})
// 判断是否真的发生了变化
if (
currentOptionChecked?.value !==
data.options.find((option) => option.key === model.activeOption?.key)?.items.find((item) => item.checked)
?.value
) {
model.activeOption?.onChange?.(
{
key: model.activeOption.key,
value: toRaw(model.activeOption.items.find((item) => item.checked)),
},
data.options.map((option) => {
return {
key: option.key,
value: toRaw(option.items.find((item) => item.checked)),
}
}),
)
}
closePicker()
}
function closePicker() {
model.activeOption = null
model.show = false
}
emits('register', {
setProps,
getProp,
toggleShow,
setOptionItems,
setOptionItemChecked,
})
</script>
<template>
<view class="wrap affix-filter" :class="[data.layout, data.style]">
<!-- -->
<view class="filter-item flex-center" v-for="option in data.options" :key="option.key">
<view class="fui-filter__item flex-center" @tap="optionClick(option)">
<text>{{ getItemLabel(option) }}</text>
<view class="ml-10rpx icon" :class="{ open: model.activeOption?.key === option.key }">
<fui-icon name="turningdown" :size="32" color="#999" />
</view>
</view>
</view>
<!-- 复用选择器 -->
<fui-picker
linkage
:options="model.activeOption?.items"
:show="model.show"
@change="optionChange"
@cancel="closePicker"
/>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.affix-filter {
display: flex;
box-sizing: border-box;
position: absolute;
top: v-bind(positionTop);
left: v-bind(positionLeft);
z-index: v-bind(zIndex);
&.vertical {
flex-direction: column;
.filter-item + .filter-item {
margin-top: 30rpx;
}
}
&.horizontal {
.filter-item + .filter-item {
margin-left: 30rpx;
}
}
&.bar {
width: 100%;
background-color: white;
left: 0;
padding: 10rpx 30rpx;
.filter-item {
flex: 1;
box-shadow: none !important;
}
}
.filter-item {
background-color: white;
padding: 15rpx 30rpx;
font-size: 28rpx;
color: #333;
border-radius: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
.icon {
@include animate();
transform: rotate(0deg);
&.open {
transform: rotate(180deg);
}
}
}
}
</style>
import { registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { AffixFilterInstance, AffixFilterProps, OptionItem } from './types'
// 组件名称
export const name = 'AffixFilterWidget'
/**
* 吸附过滤器组件
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useAffixFilterWidget<T extends AffixFilterInstance, P extends AffixFilterProps>(
props: P,
): [(instance: T) => void, AffixFilterInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
setOptionItem: (key: string, item: OptionItem) => get()?.setOptionItem(key, item),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface CheckedOption {
key: string
value: OptionItem
}
export interface OptionItem {
text: string
value: string
checked?: boolean
disabled?: boolean
[key: string]: any
}
export interface Option {
// 标识
key: string
// 提示信息
placeholder?: string
// 选项
items: OptionItem[]
// 选项改变时的回调
onChange?: (checkedOption: CheckedOption, checkedOptions: CheckedOption[]) => void
}
export interface AffixFilterProps extends BasicWidgetProps {
/**
* 布局方式,水平或垂直
* @default horizontal
*/
layout?: 'horizontal' | 'vertical'
/**
* 风格,按钮或者条形
* @default button
*/
style?: 'button' | 'bar'
/**
* 距离顶部的距离 rpx
* @default 130rpx
*/
top?: number
/**
* 距离左侧的距离 rpx
* @default 30rpx
*/
left?: number
/**
* 过滤选项集合,仅支持 Select
*/
options: Option[]
}
export interface AffixFilterInstance extends BasicWidgetInstance<AffixFilterProps> {
/**
* 设置选项
* @param key 选项标识
* @param items 选项可选 options
*/
setOptionItems: (key: string, items: OptionItem[]) => void
/**
* 设置选项选中
* @param key 选项标识
* @param value 选项值
*/
setOptionItemChecked: (key: string, value: string) => void
}
export type * from './src/types'
export * from './src/hook'
export { default as BottomBarWidget } from './src/BottomBar.vue'
/**
* 底部工具栏组件
*
* 说明: 此组件仅提供一个可交互的容器,用于放置其他组件,如: Tabs、Button 等一些自定义场景
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 1. 降水监测
* 2. 气温监测
* 3. 相对湿度
* 4. 雷达拼图
* 5. 强对流监测
* 6. 冰雪天气
* 7. 台风监测
* 8. 卫星云图
* 9. 环境监测
* 10. 山洪地质灾害、面雨量相关
* 11. 相关预报模块
* ...
*
*/
<!-- 底部 Bar 组件 -->
<script setup lang="ts">
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 是否展开
expand: {
type: Boolean,
default: false,
},
// 展开按钮标题
expandTitle: {
type: String,
},
// 是否有展开按钮
showExpandButton: {
type: Boolean,
default: true,
},
// 是否有展开按钮下边框
showExpandBorder: {
type: Boolean,
default: false,
},
// 高度
height: {
type: Number,
default: 0,
},
// 最高高度
maxHeight: {
type: Number,
default: 0,
},
})
const emits = defineEmits(['register'])
const iHeight = computed(() => `${data.height}rpx`)
const iMaxHeight = computed(() => `${data.maxHeight}rpx`)
const data = reactive({
show: props.show,
expand: props.expand,
expandTitle: props.expandTitle,
showExpandButton: props.showExpandButton,
showExpandBorder: props.showExpandBorder,
height: props.height,
maxHeight: props.maxHeight,
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function toggleExpand(expand?: boolean) {
if (data.showExpandButton) {
data.expand = expand ?? !data.expand
}
}
function setProps(value: Partial<typeof props>) {
Object.assign(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
emits('register', {
setProps,
getProp,
toggleShow,
toggleExpand,
// TODO: 此处高度还需考虑底部安全区
height: computed(() => (data.show ? (data.expand ? data.maxHeight : data.height) : 0)),
})
</script>
<template>
<view class="wrap bottom-bar" :class="{ expand: data.expand }" v-show="data.show">
<view
class="action flex-center"
:class="{ border: data.showExpandBorder }"
v-show="data.showExpandButton"
@tap="toggleExpand()"
>
<text class="text" v-if="data.expandTitle">{{ data.expandTitle }}</text>
<Icon icon="solar-double-alt-arrow-up-line-duotone" size="42" color="#999" class="icon" />
</view>
<!-- 自定义内容 -->
<scroll-view scroll-y class="content flex-center">
<slot></slot>
</scroll-view>
<!-- 底部安全区 -->
<fui-safe-area />
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.wrap {
@include animate();
position: absolute;
left: 30rpx;
bottom: 30rpx;
z-index: 99;
background-color: white;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
// padding: 30rpx 12rpx;
padding: 10px 12px;
// width: calc(100% - 60rpx);
width: 100%;
height: v-bind(iHeight);
max-height: v-bind(iMaxHeight);
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
box-sizing: border-box;
&.expand {
height: v-bind(iMaxHeight);
.icon {
transform: rotateX(180deg) !important;
}
}
.action {
width: 100%;
position: relative;
// top: -15rpx;
font-size: 24rpx;
color: #999;
padding-bottom: 10rpx;
&.border {
border-bottom: 2rpx solid #f6f6f6;
}
.text {
margin-right: 0.5em;
}
.icon {
@include animate();
transform: rotateX(0);
transform-style: preserve-3d;
}
}
.content {
box-sizing: border-box;
width: 100%;
// height: calc(100% - 60rpx - 20rpx);
height: 100%;
overflow: hidden;
position: relative;
}
}
</style>
import { getBooleanOrDefault, registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { BottomBarInstance, BottomBarProps } from './types'
// 组件名称
export const name = 'BottomBarWidget'
/**
* 底部交互/展示小部件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useBottomBarWidget<T extends BottomBarInstance, P extends BottomBarProps>(
props: P,
): [(instance: T) => void, BottomBarInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
height: computed(() => instanceRef?.value?.height) as unknown as ComputedRef<number>,
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface BottomBarProps extends BasicWidgetProps {
/**
* 是否展开
* @default false
*/
expand?: boolean
/**
* 展开标题
*/
expandTitle?: string
/**
* 是否有展开按钮
* @default true
*/
showExpandButton?: boolean
/**
* 是否有展开边框
* @default false
*/
showExpandBorder?: boolean
/**
* 高度 rpx
* @default 0 rpx
*/
height: number
/**
* 最大高度 rpx
* @default 0 rpx
*/
maxHeight?: number
}
export interface BottomBarInstance extends BasicWidgetInstance<BottomBarProps> {
/**
* 响应式组件当前高度,单位 rpx
*/
height: ComputedRef<number>
/**
* 切换展开状态
* @param expand 是否展开
*/
toggleExpand: (expand?: boolean) => void
}
export type * from './src/types'
export * from './src/hook'
export { default as LegendWidget } from './src/Legend.vue'
/**
* 图例组件
*
* 说明: 配合地图上的数据展示的图例,支持动态配置
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到图例展示的模块
* 各种场景下的配置项,可以参考: \src\pages\business\monitor\rain\config.ts defaultLegendConfig
*
*/
<!-- 图例组件 -->
<script setup lang="ts">
import { isArray } from 'lodash-es'
import { createReusableTemplate } from '@vueuse/core'
import type { Option } from './types'
import { cssAdditionCalc } from '@/components/Map/Widgets/utils'
const props = defineProps({
// 显示
show: {
type: Boolean,
default: true,
},
// 展开
expand: {
type: Boolean,
default: false,
},
// 标题
title: {
type: String,
},
// 底部距离 rpx
bottom: {
type: Number,
default: 0,
},
// JSON 数据
option: {
type: Object as PropType<Option>,
default: () => ({}),
},
})
const emits = defineEmits(['register'])
// 定义复用渲染组件
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{ item: Recordable; sub?: boolean }>()
const positionBottom = computed(() => cssAdditionCalc(30, data.bottom))
const data = reactive({
show: props.show,
expand: props.expand,
title: props.title,
option: props.option,
bottom: props.bottom,
})
function setTitle(title: string) {
data.title = title
}
function setOption(option: Option) {
data.option = option
}
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function toggleExpand(expand?: boolean) {
data.expand = expand ?? !data.expand
}
function setProps(value: Partial<typeof props>) {
Object.assign(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
emits('register', {
setProps,
getProp,
setTitle,
setOption,
toggleShow,
toggleExpand,
})
</script>
<template>
<view class="wrap legend flex-center" @click="data.expand = !data.expand">
<!-- 展开/收起 -->
<view class="expand-action flex-center" v-show="!data.expand">
<view class="text">图例</view>
<Icon icon="ic-round-keyboard-arrow-up" color="#1890ff" size="42" />
</view>
<!-- 内容 -->
<view class="expand-content flex-center" v-show="data.expand">
<!-- 标题 -->
<view v-if="data.title" class="title">{{ data.title }}</view>
<!-- 定义复用渲染组件 -->
<DefineTemplate v-slot="{ item, sub }">
<view class="item">
<!-- 图标 -->
<view v-if="item.icon" class="icon flex-center">
<CacheImage :src="item.icon" width="34" height="34" background="transparent" />
</view>
<!-- 色块 -->
<view
v-if="item.color"
class="color"
:style="{ backgroundColor: item.color, ...data.option.blockStyle }"
/>
<!-- 标签 -->
<view
v-if="item.label"
class="label"
:class="{ 'color-label': item.color }"
:style="!sub && { ...data.option.labelStyle }"
>
{{ item.label }}
</view>
<!-- 子项集合 -->
<view v-if="item.items" class="sub-items">
<view v-for="(subItem, index) in item.items" :key="index" class="item">
<ReuseTemplate :item="subItem" :sub="true" />
</view>
</view>
</view>
</DefineTemplate>
<!-- JSON -->
<view class="option">
<template v-if="isArray(data.option.items?.[0])">
<view v-for="(item, index) in data.option.items" :key="index" class="item-wrap">
<view v-for="(subItem, subIndex) in item" :key="subIndex" class="items">
<ReuseTemplate :item="subItem" />
</view>
</view>
</template>
<template v-else>
<view class="item-wrap">
<view v-for="(item, index) in data.option.items" :key="index" class="items">
<ReuseTemplate :item="item" />
</view>
</view>
</template>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.wrap {
@include animate();
position: absolute;
left: 30rpx;
bottom: v-bind(positionBottom);
z-index: 99;
}
.legend {
flex-direction: column;
padding: 12rpx 20rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.2);
.expand-action {
font-size: 26rpx;
font-weight: bold;
color: #1890ff;
.text {
margin-right: 6rpx;
}
}
.expand-content {
flex-direction: column;
font-size: 24rpx;
color: #555;
.title {
margin-bottom: 15rpx;
font-weight: bold;
}
.option {
display: flex;
.item-wrap {
+ .item-wrap {
margin-left: 15rpx;
}
.item {
display: flex;
align-items: center;
justify-content: flex-start;
.icon {
margin-right: 8rpx;
padding: 4rpx;
}
.color {
width: 38rpx;
height: 24rpx;
margin-right: 8rpx;
}
.label {
color: #777;
letter-spacing: 1px;
white-space: pre-wrap;
text-align: center;
&.color-label {
height: 24rpx;
}
}
}
.sub-items {
display: flex;
.item {
flex-direction: column;
justify-content: center;
margin: 2rpx 5rpx;
font-size: 20rpx;
}
}
}
}
}
}
</style>
import { getBooleanOrDefault, registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { LegendInstance, LegendProps, Option } from './types'
// 组件名称
export const name = 'LegendWidget'
/**
* 图例组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useLegendWidget<T extends LegendInstance, P extends LegendProps>(
props: P,
): [(instance: T) => void, LegendInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
setTitle: (title: string) => get()?.setTitle(title),
setOption: (option: Option) => get()?.setOption(option),
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface OptionItem {
// 颜色
color?: string
// 图标
icon?: string
// 标签
label?: string
// 多子项
items?: OptionItem[]
}
export interface Option {
// 选项
items: OptionItem[] | OptionItem[][]
// label 样式
labelStyle?: Recordable
// 色块样式
blockStyle?: Recordable
}
export interface LegendProps extends BasicWidgetProps {
/**
* 是否展开
* @default false
*/
expand?: boolean
/**
* 标题
*/
title: string
/**
* 选项
*/
option: Option
/**
* 底部距离 rpx
* @default 0
*/
bottom?: number
}
export interface LegendInstance extends BasicWidgetInstance<LegendProps> {
/**
* 设置标题
* @param title 标题
*/
setTitle: (title: string) => void
/**
* 设置选项
* @param option 选项
*/
setOption: (option: Option) => void
/**
* 切换展开状态
* @param expand 是否展开
*/
toggleExpand: (expand?: boolean) => void
}
export type * from './src/types'
export * from './src/hook'
export { default as SwitchWidget } from './src/Switch.vue'
/**
* 前后切换组件
*
* 说明: 用于地图页面两侧的前后切换组件,通常与 TimeBarWidget 配合使用
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到前后切换时间的场景,实况、预报等
*
*/
<!-- 前后切换组件 -->
<script setup lang="ts">
const props = defineProps({
// 是否显示
show: {
type: Boolean,
default: true,
},
// 底部距离 rpx
bottom: {
type: Number,
default: 0,
},
})
const emits = defineEmits(['register', 'prev', 'next'])
const height = computed(() => `calc(50% - 60rpx${data.bottom ? ` - ${data.bottom / 2}rpx` : ''})`)
const data = reactive({
show: props.show,
bottom: props.bottom,
prev: () => {},
next: () => {},
})
function toggleShow(show?: boolean) {
data.show = show ?? !data.show
}
function setProps(value: Partial<typeof props>) {
Object.assign(data, value)
}
function getProp(key: keyof typeof props) {
return data[key]
}
function onPrev() {
data.prev?.()
emits('prev')
}
function onNext() {
data.next?.()
emits('next')
}
emits('register', {
setProps,
getProp,
toggleShow,
prev: onPrev,
next: onNext,
})
</script>
<template>
<view class="wrap switch-control">
<view class="flex-center prev" @tap="onPrev">
<Icon class="icon" icon="ic-baseline-arrow-back-ios" size="50" color="white" />
</view>
<view class="flex-center next" @tap="onNext">
<Icon class="icon" icon="ic-baseline-arrow-forward-ios" size="50" color="white" />
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../common.scss';
.switch-control {
> * {
@include animate();
width: 60rpx;
height: 120rpx;
background-color: rgba(0, 0, 0, 0.3);
position: absolute;
top: v-bind(height);
z-index: 99;
}
.prev {
left: 0;
border-radius: 0 15rpx 15rpx 0;
.icon {
position: relative;
left: 12rpx;
}
}
.next {
right: 0;
border-radius: 15rpx 0 0 15rpx;
}
}
</style>
import { registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { SwitchControlInstance, SwitchControlProps } from './types'
// 组件名称
export const name = 'SwitchControlWidget'
/**
* 前后切换组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useSwitchWidget<T extends SwitchControlInstance, P extends SwitchControlProps>(
props: P,
): [(instance: T) => void, SwitchControlInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
prev: () => get()?.prev(),
next: () => get()?.next(),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface SwitchControlProps extends BasicWidgetProps {
// 底部距离 rpx
bottom?: number
// 上一个
prev?: () => void
// 下一个
next?: () => void
}
export interface SwitchControlInstance extends BasicWidgetInstance<SwitchControlProps> {
/**
* 手动调用上一个
*/
prev: () => void
/**
* 手动调用下一个
*/
next: () => void
}
export type * from './src/types'
export * from './src/hook'
export { default as TimeBarWidget } from './src/TimeBar.vue'
/**
* 时间栏组件
*
* 说明: 用于页面顶部的时间选择和过滤
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到时间轴的场景,实况、预报以及部分非地图的内页,均可以实现适配
*
*/
import type { Dayjs } from 'dayjs'
import { registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { TimeBarInstance, TimeBarProps } from './types'
// 组件名称
export const name = 'TimeBarWidget'
/**
* 顶部时间栏组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useTimeBarWidget<T extends TimeBarInstance, P extends TimeBarProps>(
props: P,
): [(instance: T) => void, TimeBarInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
getTime: () => get()?.getTime(),
setTime: (time: Dayjs[]) => get()?.setTime(time),
getCheckedOption: () => get()?.getCheckedOption(),
setCheckedOption: (index: number) => get()?.setCheckedOption(index),
getTimeBarValue: () => get()?.getTimeBarValue(),
},
]
}
/**
* 格式化时间
* @param time 时间
* @param format 格式化
* @returns 格式化后的时间,如果时间为空则返回空字符串
*/
export function formatTime(time: Dayjs, format = 'YYYY-MM-DD HH:mm:ss') {
return time?.format(format) ?? ''
}
/**
* 根据 type 属性返回对应的格式化字符串
* @param type fui-date-picker 组件的 type 属性
* @returns 根据 type 属性返回对应的格式化字符串
*/
export function getFormatByType(type: number) {
switch (type) {
case 1:
return 'YYYY'
case 2:
return 'YYYY-MM'
case 3:
return 'YYYY-MM-DD'
case 4:
return 'YYYY-MM-DD HH:00:00'
case 5:
return 'YYYY-MM-DD HH:mm:00'
case 6:
return 'YYYY-MM-DD HH:mm:ss'
default:
return 'YYYY-MM-DD HH:mm:ss'
}
}
import type { Dayjs } from 'dayjs'
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
/**
* 对应 first-ui datePicker 组件 type 参数
*/
type FirstUIDatePickerType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
export interface TimeBarChangeEvent {
value: Dayjs[]
option?: TimeBarLabel['options'][0]
}
export interface TimeBarLabel {
text?: string
breforeIcon?: boolean
afterIcon?: boolean
options?: {
text?: string
value?: string
checked?: boolean
format?: string
timeType?: FirstUIDatePickerType
}[]
onClick?: () => void
onChange?: (e: TimeBarChangeEvent) => void
}
export interface TimeBarTime {
type: 'single' | 'range'
timeType: FirstUIDatePickerType
format?: string
value?: Dayjs[]
onChange?: (e: TimeBarChangeEvent) => void
}
export interface TimeBarButton {
/**
* 图标,仅支持 fui-icon name
*/
icon?: string
label: string
onClick?: (e: { index: number; label: string }, time: TimeBarChangeEvent) => void
}
export interface TimeBarProps extends BasicWidgetProps {
/**
* 是否只读
* @default false
*/
readonly?: boolean
/**
* 对齐方式
* @default left
*/
align?: 'left' | 'center' | 'right'
/**
* 标题
* @default { text: '时间' }
*/
label?: TimeBarLabel
/**
* 时间
*/
time: TimeBarTime
/**
* 扩展按钮
*/
buttons?: TimeBarButton[]
}
export interface TimeBarInstance extends BasicWidgetInstance<TimeBarProps> {
/**
* 获取时间
* @returns 时间
*/
getTime: () => Dayjs[]
/**
* 设置时间
* @param time 时间
*/
setTime: (time: Dayjs[]) => void
/**
* 获取选中的选项
* @returns 选中的选项
*/
getCheckedOption: () => TimeBarLabel['options'][0]
/**
* 设置选中的选项
* @param index 选项索引
*/
setCheckedOption: (index: number) => void
/**
* 获取时间栏的值
* @returns 时间栏的值
*/
getTimeBarValue: () => TimeBarChangeEvent
}
export type * from './src/types'
export * from './src/hook'
export { default as ToolBoxWidget } from './src/ToolBox.vue'
/**
* 工具箱组件
*
* 说明: 用于展示地图上的右侧的工具箱,支持的组件有: Select、Filter、Button 之类的实现
*
* 可以应用到的一些模块:(基于已有蓝湖设计稿评估,仅供参考)
* 所有需要用到工具箱的场景,实况、预报,以及部分需要使用 AffixFilter 的场景,也可以考虑并入到工具箱中
*
*/
import { getBooleanOrDefault, registerWidgetFactory, unpackWidgetInstance } from '../../utils'
import type { ToolBoxInstance, ToolBoxProps } from './types'
// 组件名称
export const name = 'ToolBoxWidget'
/**
* 左侧工具盒子组件响应式 Hook
* @param props 组件参数
* @returns 组件响应式数据
*/
export function useToolBoxWidget<T extends ToolBoxInstance, P extends ToolBoxProps>(
props: P,
): [(instance: T) => void, ToolBoxInstance] {
const instanceRef = ref<T>()
const register = registerWidgetFactory(instanceRef, props)
const { get, ...fns } = unpackWidgetInstance(instanceRef, name)
return [
register,
{
...fns,
toggleExpand: (expand?: boolean) => get()?.toggleExpand(getBooleanOrDefault(expand)),
},
]
}
import type { BasicWidgetInstance, BasicWidgetProps } from '../../utils'
export interface ToolBoxButtonHandleEvent {
// 事件类型
type: 'click' | 'change'
// 事件名称
name: string
// 事件值
value?: string | string[] | { text: string; value: string } | { text: string; value: string }[]
}
export interface ToolBoxButton {
// 按钮类型
type: 'select' | 'button' | 'filter'
// 按钮名称
name: string
// 按钮图标
icon?: string
// 按钮是否禁用
disabled?: boolean
// select 类型按钮选项
options?: {
id?: string
// 选项名称
text: string
// 选项值
value: string
// 是否选中
checked?: boolean
// 是否禁用
disabled?: boolean
}[]
// select 类型下是否支持多选
multiple?: boolean
// 按钮事件
handle?: (e: ToolBoxButtonHandleEvent) => void
// 按钮值
value?: string | string[]
// filter 类型的结果
result?: string | string[]
}
export interface ToolBoxButtonGroup {
// 标识
key: string
// 对齐方向
align?: 'top' | 'bottom'
// 按钮列表
buttons: ToolBoxButton[]
}
export interface ToolBoxProps extends BasicWidgetProps {
/**
* 是否展开
* @default true
*/
expand?: boolean
/**
* 是否显示是否展开按钮
* @default true
*/
showExpandButton?: boolean
/**
* 顶部距离 rpx
* @default 0
*/
top?: number
/**
* 底部距离 rpx
* @default 0
*/
bottom?: number
/**
* 底部内边距 rpx
* @default 0
*/
bottomPadding?: number
/**
* 工具集
*/
tools?: ToolBoxButtonGroup[]
}
export interface ToolBoxInstance extends BasicWidgetInstance<ToolBoxProps> {
/**
* 展开或收起工具箱
* @param expand 是否展开
*/
toggleExpand: (expand?: boolean) => void
}
@mixin animate() {
transition: all 0.35s;
}
import { isBoolean } from 'lodash-es'
import { isProdMode } from '@/utils/env'
export interface BasicWidgetProps {
/**
* 是否显示
* @default true
*/
show?: boolean
}
export interface BasicWidgetInstance<T extends BasicWidgetProps = BasicWidgetProps> {
/**
* 设置小部件属性
* @param props 小部件属性
*/
setProps: (props: Partial<T>) => void
/**
* 获取小部件属性
* @param key 属性名称
* @returns 属性值
*/
getProp: <K extends keyof T>(key: K) => T[K]
/**
* 切换显示状态
* @param show 是否显示,强制指定显示状态
*/
toggleShow: (show?: boolean) => void
}
/**
* 获取 Boolean 值或默认值
* @param value Boolean 值
* @param def 默认值
* @returns 值或默认值
*/
export function getBooleanOrDefault(value: any, def = null): boolean {
return isBoolean(value) ? value : def
}
/**
* 注册组件工厂
* @param instanceRef 组件实例 Ref
* @param props 组件属性
* @param callback 回调函数
*/
export function registerWidgetFactory<T extends BasicWidgetInstance<P>, P>(
instanceRef: Ref<Nullable<T>>,
props: P,
callback?: (instance: T) => void,
): (instance: T) => void {
function register(instance: T) {
if (isProdMode()) {
if (instance === unref(instanceRef)) {
return
}
onUnmounted(() => {
instanceRef.value = null
})
}
instanceRef.value = instance
props && instance?.setProps(props)
callback && callback(instance)
}
return register
}
/**
* 获取组件实例
* @param instanceRef 组件实例 Ref
* @param name 组件名称
* @returns 组件实例
*/
export function getWidgetInstance<T extends BasicWidgetInstance>(instanceRef: Ref<Nullable<T>>, name?: string): T {
const instance = unref(instanceRef)
if (!instance) {
console.warn(`The ${name || 'widget'} instance has not been obtained`)
}
return instance as T
}
/**
* 获取基础组件实例函数
* @param instanceRef 组件实例 Ref
* @param name 组件名称
* @returns 组件实例函数
* @example
* ```ts
* const { get, setProps, toggleShow } = unpackWidgetInstance<BasicWidgetInstance, BasicWidgetProps>(instanceRef, 'basic widget')
* ```
*/
export function unpackWidgetInstance<T extends BasicWidgetInstance, P extends BasicWidgetProps>(
instanceRef: Ref<Nullable<T>>,
name?: string,
) {
const get = () => getWidgetInstance(instanceRef, name)
return {
get,
setProps: (props: Partial<P>) => get()?.setProps(props),
// @ts-expect-error
getProp: <K extends keyof P>(key: K) => get()?.getProp(key),
toggleShow: (show?: boolean) => get()?.toggleShow(getBooleanOrDefault(show)),
} as T & { get: () => T }
}
/**
* css calc 加法计算函数
* @param values rpx 数值
* @returns css calc 函数或 rpx 数值
*/
export function cssAdditionCalc(...values: number[]): string {
const result = values
.filter((item) => item)
.map((item) => `${item}rpx`)
.join(' + ')
if (result.includes('+')) {
return `calc(${result})`
} else {
return result
}
}
<script setup lang="ts">
defineProps({
minmaxData: {
type: Array,
default: () => [
{ label: '最大', value: '0' },
{ label: '最小', value: '0' },
],
},
})
</script>
<template>
<view class="tipgridwrap">
<view v-for="(item, index) in minmaxData" :key="index" class="tipwrap" style="flex-basis: 50%">
<view class="tipflex tiptitle">{{ item.label }}</view>
<view class="tipflex tipcontent">{{ item.value }}</view>
</view>
</view>
</template>
<style lang="less" scoped>
.tipgridwrap {
padding: 5px 10px;
display: grid;
grid-template-columns: 49% 49%;
grid-column-gap: 10px;
.tipwrap {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
margin-top: 5px;
border: 1px solid rgb(24 144 255 / 20%);
border-radius: 5px;
overflow: hidden;
.tipflex {
flex-basis: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 26rpx;
}
.tiptitle {
height: 100%;
background: #1890ff;
color: #fff;
}
.tipcontent {
height: 100%;
background: #fff;
}
}
}
</style>
<script setup lang="ts">
import { windheaderData, windtableData } from '@/pages/business/monitor/wind/config'
defineProps({
tableData: {
type: Array,
default: () => windtableData,
},
headerData: {
type: Array,
default: () => windheaderData,
},
})
</script>
<template>
<view @click="clickTest" class="tablewrap">
<fui-table
:itemList="tableData"
:header="headerData"
size="24"
textSize="24"
color="#1890FF"
textColor="#666666"
headerBgColor="rgba(24,144,255,0.08)"
gap="30"
padding="14"
borderColor="rgba(24,144,255,0.08)"
/>
</view>
</template>
<style lang="less" scoped>
.tablewrap {
margin-top: 5px;
}
</style>
......@@ -7,7 +7,7 @@ export function useConcealedExit(max = 10) {
const maxTapCount = max
const count = ref(0)
let loop: NodeJS.Timer
let loop: NodeJS.Timeout
// 隐藏退出功能计数
function exit() {
......
/**
* 页面分享 Hook
* @param index 分享按钮索引
* @param cb 自定义回调
* @returns 分享函数
*/
export function useShare(index = 0, cb?: (e: Page.NavigationBarButtonTapOption) => void) {
/**
* 分享
*/
function share() {
// TODO
// 1. 获取需要分享的类型,如: 图片、链接、文本
// 2. 根据类型,获取需要分享的内容(图片通常是页面截屏)
// 3. 实现截屏功能
// 4. 实现分享功能
Message.toast('分享功能暂未实现')
}
onNavigationBarButtonTap((e) => {
if (e.index === index) {
share()
}
// 自定义回调
cb?.(e)
})
return {
share,
}
}
{
"name" : "Beta App",
"appid" : "__UNI__2E9441A",
"description" : "APP 基础工程",
"versionName" : "3.0.3.0",
"versionCode" : 112,
"name" : "湖南天气",
"appid" : "__UNI__7842A65",
"description" : "湖南天气 APP v5",
"versionName" : "5.0.0",
"versionCode" : 500,
"transformPx" : false,
"locale" : "zh-Hans",
/* 5+App特有相关 */// 配置文件详细说明
......
{
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#FFFFFF",
"pageOrientation": "portrait", //横屏配置,全局屏幕旋转设置(仅 APP/微信/QQ小程序),支持 auto / portrait / landscape
"app-plus": {
"scrollIndicator": "none",
"titleNView": {
"titleSize": "20"
}
}
},
"pages": [
// pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": false,
// #ifdef H5
"navigationStyle": "custom",
// #endif
"app-plus": {
"titleNView": false
},
"enableReachBottom": false
}
},
{
"path": "pages/business/index",
"style": {
"navigationBarTitleText": "监测预报",
"enablePullDownRefresh": false,
"navigationBarBackgroundColor": "#1890FF",
"app-plus": {
"titleNView": {
"backgroundImage": "/static/warnforecast.png",
"backgroundRepeat": "no-repeat"
}
},
"SplitLineStyles": {
"height": "160px"
}
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "消息",
"enablePullDownRefresh": false,
"navigationBarBackgroundColor": "#1890FF",
"app-plus": {
"titleNView": {
"backgroundImage": "/static/warnforecast.png",
"backgroundRepeat": "no-repeat"
}
}
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的",
"enablePullDownRefresh": false,
// #ifdef H5
"navigationStyle": "custom",
// #endif
"app-plus": {
"titleNView": false
},
"enableReachBottom": false
}
},
// 降水监测
{
"path": "pages/business/monitor/rain/index",
"style": {
"navigationBarTitleText": "Basic APP"
"navigationBarTitleText": "降水监测(演示)",
"enablePullDownRefresh": false,
"navigationBarBackgroundColor": "#1890FF",
"app-plus": {
"titleNView": {
"buttons": [
{
"type": "share",
"color": "white",
"width": "50px"
}
]
}
}
}
},
// 气温监测
{
"path": "pages/business/monitor/tem/index",
"style": {
"navigationBarTitleText": "气温监测",
"enablePullDownRefresh": false,
"navigationBarBackgroundColor": "#1890FF",
"app-plus": {
"titleNView": {
"buttons": [
{
"type": "share",
"color": "white",
"width": "50px"
}
]
}
}
}
},
// 大风监测
{
"path": "pages/business/monitor/wind/index",
"style": {
"navigationBarTitleText": "大风监测",
"enablePullDownRefresh": false,
"navigationBarBackgroundColor": "#1890FF",
"app-plus": {
"titleNView": {
"buttons": [
{
"type": "share",
"color": "white",
"width": "50px"
}
]
}
}
}
},
// === Webview ===
{
"path": "pages/common/webview/index",
"style": {
"titleNView": true,
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#1890FF",
"enablePullDownRefresh": false
}
},
// === PDF 文件预览 ===
{
"path": "pages/common/viewer/pdf",
......@@ -60,11 +194,41 @@
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
"resizable": true,
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#1890FF",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"height": "60px",
"fontSize": "10px",
"iconWidth": "24px",
"spacing": "6px",
"list": [
{
"pagePath": "pages/home/index",
"iconPath": "static/icons/home.png",
"selectedIconPath": "static/icons/homeSelect.png",
"text": "首页"
},
{
"pagePath": "pages/business/index",
"iconPath": "static/icons/monitorforecast.png",
"selectedIconPath": "static/icons/monitorforecastSelect.png",
"text": "监测预报"
},
"resizable": true
{
"pagePath": "pages/message/index",
"iconPath": "static/icons/message.png",
"selectedIconPath": "static/icons/messageSelect.png",
"text": "消息"
},
{
"pagePath": "pages/mine/index",
"iconPath": "static/icons/mine.png",
"selectedIconPath": "static/icons/mineSelect.png",
"text": "我的"
}
]
}
}
<script setup lang="ts">
import { climateModules, forecastModules, liveModules, weatherModules } from '@/utils/const/Navigation'
const navs = [
{ dataSouce: liveModules, title: '实况监测' },
{ dataSouce: forecastModules, title: '预报信息' },
{ dataSouce: climateModules, title: '气候' },
{ dataSouce: weatherModules, title: '天气' },
]
onMounted(() => {})
</script>
<template>
<view class="">
<!-- 头部 -->
<!-- <Header title="监测预报" /> -->
<!-- 内容 -->
<view class="p-2 pt-3 pb-1">
<view class="box-shadow-def p-2 pt-3 b-rd-2 mb-3 bg-#fff" v-for="(item, index) in navs" :key="index">
<MenuHeader :title="item.title" otherTitle="展开更多" />
<Grid :dataSouce="item.dataSouce" />
</view>
</view>
</view>
</template>
// 页面通用配置信息或一些抽象方法
// 单色块图例配置
// export const defaultLegendConfig = {
// expand: false,
// title: '单位: mm',
// option: {
// blockStyle: {
// border: '1px solid #ccc',
// },
// items: [
// {
// color: '#FFFFFF',
// label: '0-0.1',
// },
// {
// color: '#A3F48A',
// label: '0.1-9.0',
// },
// {
// color: '#35AB02',
// label: '10-24.9',
// },
// {
// color: '#61B9F9',
// label: '25-49.9',
// },
// {
// color: '#0403FF',
// label: '50-99.9',
// },
// {
// color: '#FA02FA',
// label: '100-249.9',
// },
// {
// color: '#810042',
// label: '>=250',
// },
// ],
// },
// }
// 多色块图例配置
// export const defaultLegendConfig = {
// expand: true,
// title: '单位: mm',
// option: {
// blockStyle: {
// border: '1px solid #ccc',
// },
// items: [
// [
// {
// color: '#FFFFFF',
// label: '0-0.1',
// },
// {
// color: '#A3F48A',
// label: '0.1-9.0',
// },
// {
// color: '#35AB02',
// label: '10-24.9',
// },
// {
// color: '#61B9F9',
// label: '25-49.9',
// },
// {
// color: '#0403FF',
// label: '50-99.9',
// },
// {
// color: '#FA02FA',
// label: '100-249.9',
// },
// {
// color: '#810042',
// label: '>=250',
// },
// ],
// [
// {
// color: '#FFFFFF',
// label: '0-0.1',
// },
// {
// color: '#A3F48A',
// label: '0.1-9.0',
// },
// {
// color: '#35AB02',
// label: '10-24.9',
// },
// {
// color: '#61B9F9',
// label: '25-49.9',
// },
// {
// color: '#0403FF',
// label: '50-99.9',
// },
// {
// color: '#FA02FA',
// label: '100-249.9',
// },
// {
// color: '#810042',
// label: '>=250',
// },
// ],
// ],
// },
// }
// 单边图标图例
// export const defaultLegendConfig = {
// expand: true,
// title: '',
// option: {
// items: [
// {
// icon: '/static/icons/legend/ice-snow/snow.cover.png',
// label: '积雪',
// },
// {
// icon: '/static/icons/legend/ice-snow/wire.icing.png',
// label: '电线积冰',
// },
// {
// icon: '/static/icons/legend/ice-snow/glaze.png',
// label: '雨淞',
// },
// {
// icon: '/static/icons/legend/ice-snow/sleet.png',
// label: '雨夹雪',
// },
// {
// icon: '/static/icons/legend/ice-snow/snow.png',
// label: '雪',
// },
// ],
// },
// }
// 单边多图标图例
export const defaultLegendConfig = {
expand: true,
title: '',
option: {
labelStyle: {
width: '85rpx',
},
items: [
{
label: '闪电',
items: [
{
icon: '/static/icons/legend/strong-convection/lightning.png',
},
],
},
{
label: '冰雹\n(mm)',
items: [
{
icon: '/static/icons/legend/strong-convection/hail.0-5.png',
label: '0-5',
},
{
icon: '/static/icons/legend/strong-convection/hail.5-10.png',
label: '5-10',
},
{
icon: '/static/icons/legend/strong-convection/hail.10-20.png',
label: '10-20',
},
{
icon: '/static/icons/legend/strong-convection/hail.+20.png',
label: '>=20',
},
],
},
{
label: '大风\n(m/s)',
items: [
{
icon: '/static/icons/legend/strong-convection/wind.17-25.png',
label: '17-25',
},
{
icon: '/static/icons/legend/strong-convection/wind.25-30.png',
label: '25-30',
},
{
icon: '/static/icons/legend/strong-convection/wind.+30.png',
label: '>=30',
},
],
},
{
label: '强降水\n(mm)',
items: [
{
icon: '/static/icons/legend/strong-convection/rain.20-30.png',
label: '20-30',
},
{
icon: '/static/icons/legend/strong-convection/rain.30-50.png',
label: '30-50',
},
{
icon: '/static/icons/legend/strong-convection/rain.50-80.png',
label: '50-80',
},
{
icon: '/static/icons/legend/strong-convection/rain.+80.png',
label: '>=80',
},
],
},
],
},
}
/**
* TODO: 色块图例配置,实际上应该和后端的色斑图,进行动态匹配,可以考虑由后端接口返回
*/
/**
* 小时气温图例配置
*/
export const hourTemLegendConfig = {
expand: false,
title: '单位: °C',
option: {
items: [
{
color: '#800000',
label: '<44',
},
{
color: '#960000',
},
{
color: '#AB0001',
label: '40',
},
{
color: '#C80100',
},
{
color: '#E70000',
label: '36',
},
{
color: '#FE371B',
},
{
color: '#FF4B25',
label: '32',
},
{
color: '#FF7300',
},
{
color: '#FF9A02',
label: '28',
},
{
color: '#FFC300',
},
{
color: '#FFE703',
label: '24',
},
{
color: '#FFFF04',
},
{
color: '#F5FF03',
label: '20',
},
{
color: '#DEFF00',
},
{
color: '#C5FF04',
label: '16',
},
{
color: '#B5FF00',
},
{
color: '#98FD0F',
label: '12',
},
{
color: '#8AFA2D',
},
{
color: '#72F806',
label: '8',
},
{
color: '#00FF73',
},
{
color: '#00FF94',
label: '4',
},
{
color: '#00FFB4',
},
{
color: '#00FFD6',
label: '0',
},
{
color: '#06FFF7',
},
{
color: '#00EBFF',
label: '-4',
},
{
color: '#00CBFF',
},
{
color: '#01A6FF',
label: '-8',
},
{
color: '#0085FF',
},
{
color: '#0365FF',
label: '-12',
},
],
},
}
/**
* TODO: 色块图例配置,实际上应该和后端的色斑图,进行动态匹配,可以考虑由后端接口返回
*/
import { cloneDeep } from 'lodash-es'
/**
* 小时气温图例配置
*/
export const hourTemLegendConfig = {
expand: false,
title: '单位: °C',
option: {
items: [
{
color: '#800000',
label: '<44',
},
{
color: '#960000',
},
{
color: '#AB0001',
label: '40',
},
{
color: '#C80100',
},
{
color: '#E70000',
label: '36',
},
{
color: '#FE371B',
},
{
color: '#FF4B25',
label: '32',
},
{
color: '#FF7300',
},
{
color: '#FF9A02',
label: '28',
},
{
color: '#FFC300',
},
{
color: '#FFE703',
label: '24',
},
{
color: '#FFFF04',
},
{
color: '#F5FF03',
label: '20',
},
{
color: '#DEFF00',
},
{
color: '#C5FF04',
label: '16',
},
{
color: '#B5FF00',
},
{
color: '#98FD0F',
label: '12',
},
{
color: '#8AFA2D',
},
{
color: '#72F806',
label: '8',
},
{
color: '#00FF73',
},
{
color: '#00FF94',
label: '4',
},
{
color: '#00FFB4',
},
{
color: '#00FFD6',
label: '0',
},
{
color: '#06FFF7',
},
{
color: '#00EBFF',
label: '-4',
},
{
color: '#00CBFF',
},
{
color: '#01A6FF',
label: '-8',
},
{
color: '#0085FF',
},
{
color: '#0365FF',
label: '-12',
},
],
},
}
export const windheaderData = [
{
prop: '0~3级',
label: '0~3级',
width: '136',
},
{
prop: '4~7级',
label: '4~7级',
width: '136',
},
{
prop: '8~10级',
label: '8~10级',
width: '136',
},
{
prop: '11~14级',
label: '11~14级',
width: '136',
},
{
prop: '15~17级',
label: '15~17级',
width: '136',
},
]
export const windtableData = [
{
'0~3级': '0站',
'4~7级': '0站',
'8~10级': '0站',
'11~14级': '0站',
'15~17级': '0站',
},
]
export function buildQueryParams(unBuildParams, config?) {
// const params = unBuildParams
// for (const key in params) {
// if (isRef(params[key])) {
// params[key] = unref(params[key])
// }
// if (isReactive(params[key])) {
// params[key] = toRaw(params[key])
// }
// }
const queryParams = cloneDeep(toRaw(unBuildParams))
if (config?.isSpot) {
delete queryParams.numericalBegin
delete queryParams.numericalEnd
delete queryParams.symbolBegin
delete queryParams.symbolEnd
}
if (config?.isFirstQuery) {
delete queryParams.startTime
delete queryParams.endTime
}
return queryParams
}
export function createSplotLayer(type: String) {
return {
id: `${type}-splot`,
type: 'fill',
source: `${type}-splot`,
paint: {
'fill-color': {
type: 'identity',
property: 'color',
},
'fill-opacity': 1,
},
}
}
export function createTextLayer(type: String) {
return {
id: `${type}-text`,
type: 'symbol',
source: `${type}-text`,
layout: {
'text-field': '{value}',
'text-size': 14,
'text-offset': [0, 1.2],
'icon-image': 'point.normal',
'icon-anchor': 'top',
'icon-size': 0.4,
},
paint: {
'text-color': '#3766fd',
'text-halo-color': '#fff',
'text-halo-width': 1,
},
}
}
<script lang="ts" setup>
import URI from 'urijs'
import { getExtranetUrl } from '/@/utils/proxy'
const link = ref<string>('')
......
<script lang="ts" setup>
const page = reactive({
title: '',
link: '',
})
onLoad(({ title, link }) => {
page.title = title ? decodeURI(title) : ''
page.link = decodeURIComponent(link)
if (!link) {
Message.toast('页面打开失败,参数错误')
uni.navigateBack()
return
}
// 设置标题栏为文件名
if (page.title && page.title !== 'undefined') {
uni.setNavigationBarTitle({
title: page.title,
})
}
// #ifdef APP-PLUS
const webview = plus.webview.currentWebview()
webview.setStyle({ scalable: true })
// #endif
})
</script>
<template>
<view class="wrap" v-if="page.link">
<web-view :src="page.link" :update-title="!page.title" />
</view>
</template>
<script setup lang="ts">
import Header from './modules/header.vue'
import Warning from './modules/warning.vue'
import MeteService from './modules/meteService.vue'
import CommonFunction from './modules/commonFunction.vue'
onMounted(() => {
// test API
// API.example.hello.request().then((body) => {
// title.value = body
// console.log('[API]', body, $app.name, $app.version)
// Message.toast(body)
// })
})
// const surprise = () => {
// // #ifdef APP-PLUS
// const orientation = plus.navigator.getOrientation()
// if (orientation === 0) {
// plus.screen.lockOrientation('landscape-primary')
// } else if (orientation === 90) {
// plus.screen.lockOrientation('portrait-primary')
// }
// Message.toast('🥳 surprise ~ ')
// // #endif
// // #ifndef APP-PLUS
// Message.toast(`使用 APP 运行点击有惊喜~ ╰(*°▽°*)╯`)
// // #endif
// }
</script>
<template>
<view class="content">
<!-- 头部 -->
<Header />
<!-- 内容 -->
<view class="p-2 pt-3">
<Warning />
<MeteService />
<CommonFunction />
</view>
<!-- <Weather /> -->
</view>
</template>
<style lang="less">
.content {
height: calc(100vh);
overflow: auto;
background-color: #f2f2f2;
letter-spacing: 3rpx;
}
</style>
<template>
<view class="p-2 b-rd-2 box-shadow-def mt-3 bg-#fff">
<MenuHeader title="常用功能" otherTitle="自定义" />
<Grid />
</view>
</template>
<script setup lang="ts">
const city = ref<string>('长沙市')
const county = ref<string>('天心区')
const path = ref<string>('/static/header.png')
onMounted(() => {})
</script>
<template>
<view
class="text-c0 bg-cover h-362 pos-relative p-4 pt-13 pb-0 color-white"
:style="`background-image: url(${path})`"
>
<fui-row>
<!-- 定位 -->
<fui-col :span="12">
<view class="flex">
<FuiIcon custom-prefix="font_family" name="icon-location" color="#fff" size="42" />
<text class="ml-2 text-30">{{ city }} &nbsp;{{ county }}</text>
</view>
</fui-col>
<!-- 应急响应 -->
<fui-col :span="12">
<view class="flex flex-justify-end b-rd-0.7 text-24 lh-7 h-56">
<view class="pl-3 pr-4 inline-block pr-2 bg-[#f6f7fb] b-rd-l-0.7 bg-[#FF9A03]">暴雨</view>
<view
class="pos-relative pr-1.3 pl-2 inline-block bg-[#fff] text-c2 font-650 b-rd-r-0.7 triangle color-[#333333]"
>
|| 响应
</view>
</view>
</fui-col>
</fui-row>
<!-- 天气温度监测及天气类型 -->
<fui-row class="pt-6 pb-5 pl-0.5">
<!-- 天气监测 -->
<fui-col :span="9" class="h-128">
<view>
<text class="text-112">26°</text>
<text class="text-32">多云</text>
</view>
</fui-col>
<!-- 天气类型 -->
<fui-col :span="15">
<fui-row class="h-128" :isFlex="true" justify="end" align="middle">
<fui-col :span="8" v-for="item in 2" :key="item">
<view class="h-40 text-27 mr-1 p-1 pr-2 mb-1 bg-amber b-rd-10 flex-center">
<FuiIcon custom-prefix="font_family" name="icon-gw" color="#fff" size="36" />
<text class="ml-1">高温</text>
</view>
</fui-col>
</fui-row>
</fui-col>
</fui-row>
<!-- 空气质量及预报 -->
<fui-row>
<fui-col :span="12">
<view>
<text class="mr-4 text-32">18° ~ 29°</text>
<FuiIcon class="mt-1" custom-prefix="font_family" name="icon-bianzu" color="#fff" size="36" />
<text>20 优</text>
</view>
</fui-col>
<fui-col :span="12">
<view class="flex-1 flex flex-justify-end">
<text class="bg-[rgba(0,7,17,0.3)] text-28 pt-1 pb-1 pl-3 pr-1 b-rd-4">
城市预报
<fui-icon name="arrowright" :size="28" color="#fff" />
</text>
</view>
</fui-col>
</fui-row>
</view>
</template>
<style lang="less" scoped>
.triangle {
&::after {
content: '';
width: 0;
height: 0;
position: absolute;
border-style: solid;
border-color: #fff transparent;
border-width: 0 0 56rpx 20rpx;
top: 0;
left: -20rpx;
z-index: 99;
}
}
</style>
<script setup lang="ts"></script>
<template>
<view class="box-shadow-def bg-#fff b-rd-2 mt-3 p-3 pb-0">
<!-- 头部 -->
<MenuHeader />
<!-- 内容 -->
<view
class="b-0 b-t-2 b-solid pt-3"
:style="`border-color:${index !== 0 ? '#EEEEEE' : 'transparent'};`"
v-for="(item, index) in 3"
:key="item"
>
<view class="text-#333333 font-550 text-30"> 湖南省未来三天天气预报 </view>
<view class="pt-4 pb-4">
<text class="text-24 font-500 w-132 b-2 b-#1890FF b-solid b-rd-4 p-1 pl-2 pr-1 text-#3E8FFD">
决策服务
</text>
<text class="text-#999999 ml-2 text-24"> 2023/02/28 17:00 </text>
</view>
</view>
</view>
</template>
<script setup lang="ts"></script>
<template>
<view class="h-80 bg-[#FFFBE6] b-rd-5 pl-3 lh-10 flex box-shadow-def">
<view>
<image src="/static/images/earlyWarningIcon/by-1.svg" alt="by" class="w-54 h-50 mt-2 shadow-yellow" />
</view>
<view class="ml-2 overflow-hidden">
<text class="text-28 text[#000000]"> 湖南省气象台5月15日14:00发布 </text>
<text class="text-28 text-red">暴雨红色预警</text>
</view>
</view>
</template>
<script setup lang="ts">
const url = ref<string>('/static/images/weather-little-icon/day1.png')
/**
* 切换城市
*/
function cityChange() {
uni.showToast({ title: '切换城市' })
}
/**
* 预报预览
*/
function previewForecast() {
uni.showToast({ title: '预报预览' })
}
</script>
<template>
<view class="weather-container">
<view class="weather-city" @tap="cityChange">
<FuiIcon name="location" color="#3FA5FF" class="location-icon" />
<text class="location-text">天心区</text>
<text class="right-line" />
</view>
<view class="weather-info">
<image :src="url" />
<text> 晴 39°</text>
<view class="weather-air"></view>
</view>
<view class="weather-detail" @tap="previewForecast"> 未来24小时预报 > </view>
</view>
</template>
<style lang="less" scoped>
.weather-container {
display: flex;
width: 100%;
height: 70rpx;
padding: 20rpx;
align-items: center;
justify-content: center;
background-color: #fff;
.weather-city {
flex: 1;
display: flex;
align-items: center;
justify-content: left;
.location-icon {
font-size: 42rpx !important;
margin-right: 10rpx;
vertical-align: middle;
}
.location-text {
vertical-align: middle;
}
.right-line {
display: block;
width: 2px;
height: 42rpx;
background-color: #eee;
margin-left: 24rpx;
border-radius: 10%;
}
}
.weather-info {
flex: 1.3;
display: flex;
align-items: center;
justify-content: left;
image {
width: 50rpx;
height: 50rpx;
margin-right: 18rpx;
// margin-bottom: 10rpx;
}
text {
vertical-align: top;
}
.weather-air {
width: 65rpx;
// height: 42rpx;
line-height: 42rpx;
background-color: #24e228;
border-radius: 5px;
text-align: center;
font-size: 27rpx;
margin-left: 10rpx;
color: #fff;
}
}
.weather-detail {
flex: 1.4;
font-size: 28rpx;
color: #aaa;
}
}
</style>
<script setup lang="ts">
import dayjs from 'dayjs'
import { checkUpgrade } from '@/utils/upgrade'
import { useRuntime } from '@/hooks/app/useRuntime'
import { useConcealedExit } from '@/hooks/app/useConcealedExit'
const { exit } = useConcealedExit()
const { app } = useRuntime()
const year = ref(dayjs().year())
const title = ref('Hello World')
const version = computed(() => app.value.version)
onMounted(() => {
// test API
API.example.hello
.request()
.then((body) => {
title.value = body
console.log('[API]', body, $app.name, $app.version)
Message.toast(body)
})
.catch((err) => {
console.error('[API]', err)
})
})
function surprise() {
// #ifdef APP-PLUS
const orientation = plus.navigator.getOrientation()
if (orientation === 0) {
plus.screen.lockOrientation('landscape-primary')
} else if (orientation === 90) {
plus.screen.lockOrientation('portrait-primary')
}
Message.toast('🥳 surprise~')
// #endif
// #ifndef APP-PLUS
Message.toast(`在手机上运行点击有惊喜~ ╰(*°▽°*)╯`)
// #endif
}
const animate = ref()
function handUp() {
if (animate.value) {
return
}
Message.toast('👋🏻')
animate.value = 'animate-swing'
setTimeout(() => (animate.value = null), 1500)
}
</script>
<template>
<view class="content flex-center flex-col">
<fui-avatar
src="/static/logo.png"
radius="14"
size="large"
background="transparent"
class="!mb-3"
@click="surprise"
/>
<view class="flex-center flex-col mb-3">
<text class="title">{{ title }}</text>
<text class="title">{{ $t('app.hello') }}</text>
<Icon icon="emojione-grinning-face" size="48" class="mt-3" />
<fui-divider />
<uni-icons type="hand-up" size="30" :class="[animate]" @tap="handUp" />
</view>
<fui-footer isFixed>
<template #text>
<view v-if="version" @tap="checkUpgrade(true)">V{{ version }}</view>
<view class="mt-1">壹润科技 版权所有</view>
<view class="mt-1" @tap="exit">Copyright © 2021-{{ year }} Yiring. All Ringhts Reserved</view>
</template>
</fui-footer>
</view>
</template>
<style lang="less" scoped>
.content {
height: calc(100vh - 100rpx);
}
.title {
display: flex;
justify-content: center;
font-size: 36rpx;
color: #8f8f94;
}
</style>
<script setup lang="ts">
import MenuHeader from '@/components/Layout/MenuHeader.vue'
const options = ['全部消息', '应急响应', '预警信号', '临灾警报', '服务材料', '重要天气']
const value = ref<string>('全部消息')
onMounted(() => {})
</script>
<template>
<view class="bg-#fff">
<!-- 头部 -->
<!-- <Header title="消息" /> -->
<!-- tab -->
<view class="bg-#fff box-shadow-bottom pt-3.5 pl-3.2 pb-2.5">
<fui-data-tag
:modelValue="value"
:options="options"
background="#F9FAFD"
activeColor="#fff"
defaultBorderColor="#1890FF"
activeBgColor="linear-gradient(94deg, #1890FF 0%, #377ED9 100%)"
width="216"
gap="25"
/>
</view>
<!-- 内容 -->
<view class="bg-#fff p-3.5 pt-0 mt-3">
<MenuHeader title="全部消息" :otherTitleVis="false">
<template #content>
<text class="font-500 text-28 text-#1890FF">
<FuiIcon custom-prefix="font_family" name="icon-read" size="36" color="#1890FF" />
全部阅毕
</text>
</template>
</MenuHeader>
<view v-for="item in 6" :key="item" class="flex-center pt-3 pb-2 b-#EEEEEE b-solid b-0 b-b-2">
<view class="pr-5 text-left">
<text class="text-28 font-550 text-#333333"> 湖南省长沙市启动洪涝灾害 II 级 应急响应 </text>
<view class="lh-10">
<text class="b-rd-20 p-1 pl-2 pr-2 bg-#1890FF1A text-#1890FFFF text-24">未阅</text>
<text class="text-#999999FF text-24 ml-2">2023/05/23 14:00</text>
</view>
</view>
<view>
<image src="/static/images/message/fireHazard.png" class="w-204 h-154" />
</view>
</view>
</view>
</view>
</template>
export const lists = [
{
title: '应急状态管理',
icon: 'emergencyManage',
},
{
title: '用户默认层级',
icon: 'user',
},
{
title: '常用功能设置',
icon: 'common',
},
{
title: '消息推送设置',
icon: 'push',
},
{
title: '问卷调查填写',
icon: 'survey',
},
{
title: '问题意见反馈',
icon: 'feedback',
},
{
title: '关于湖南天气',
icon: 'about',
},
{
title: '值班电话号码',
icon: 'phone',
suffix: '0731-85600016',
},
{
title: '系统缓存清除',
icon: 'clear',
},
{
title: '系统检查更新',
icon: 'update',
},
]
<script setup lang="ts">
import { lists } from './constant/index'
import View from '@/components/Layout/View.vue'
const url = '/static/images/mine/provincial.png'
const bgUrl = '/static/header/mine.png'
</script>
<template>
<View>
<template #container>
<Header title="我的" class="h-405" :url="bgUrl">
<template #content>
<view
class="w-200 h-250 pos-absolute left-50% z-1 bottom-0% transform lh-8"
style="transform: translateX(-50%)"
>
<image :src="url" class="w-144 h-144" />
<text class="letter-spacing-1">18771683288</text>
</view>
</template>
</Header>
<view class="bg-#F0F2F5">
<fui-list>
<fui-list-cell arrow v-for="(item, index) in lists.slice(0, 4)" :key="index" class="border-bottom">
<FuiIcon custom-prefix="font_family" :name="`icon-${item.icon}`" color="#333333" size="42" />
<text class="ml-3.5 text-30 text-#333333 font-500">{{ item.title }}</text>
</fui-list-cell>
</fui-list>
<fui-list style="margin-top: 20rpx">
<fui-list-cell arrow v-for="(item, index) in lists.slice(4, 6)" :key="index" class="border-bottom">
<FuiIcon custom-prefix="font_family" :name="`icon-${item.icon}`" color="#333333" size="42" />
<text class="ml-3.5 text-30 text-#333333 font-500">{{ item.title }}</text>
</fui-list-cell>
</fui-list>
<fui-list style="margin-top: 20rpx">
<fui-list-cell
arrow
v-for="(item, index) in lists.slice(6, 10)"
:key="index"
class="border-bottom pos-relative"
>
<FuiIcon custom-prefix="font_family" :name="`icon-${item.icon}`" color="#333333" size="42" />
<text class="ml-3.5 text-30 text-#333333 font-500">{{ item.title }}</text>
<text v-if="item.suffix" class="text-#1890FF pos-absolute right-70">
{{ item.suffix }}
</text>
</fui-list-cell>
</fui-list>
</view>
<view class="p-6 pt-5 pb-2">
<fui-button background="#fff" radius="50rpx" color="#1890FF">退出当前账号</fui-button>
</view>
</template>
</View>
</template>
@font-face {
font-family: "font_family"; /* Project id 4105118 */
src: url('//at.alicdn.com/t/c/font_4105118_j7v7r42023c.woff2?t=1687340820798') format('woff2'),
url('//at.alicdn.com/t/c/font_4105118_j7v7r42023c.woff?t=1687340820798') format('woff'),
url('//at.alicdn.com/t/c/font_4105118_j7v7r42023c.ttf?t=1687340820798') format('truetype');
}
.font_family {
font-family: "font_family" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-about:before {
content: "\e639";
}
.icon-feedback:before {
content: "\e63a";
}
.icon-clear:before {
content: "\e63b";
}
.icon-common:before {
content: "\e63c";
}
.icon-update:before {
content: "\e63d";
}
.icon-user:before {
content: "\e63e";
}
.icon-phone:before {
content: "\e63f";
}
.icon-emergencyManage:before {
content: "\e640";
}
.icon-survey:before {
content: "\e641";
}
.icon-push:before {
content: "\e642";
}
.icon-read:before {
content: "\e638";
}
.icon-areal-rainfall-m:before {
content: "\e616";
}
.icon-radar-m:before {
content: "\e617";
}
.icon-geology-disaster-m:before {
content: "\e618";
}
.icon-rain-m:before {
content: "\e619";
}
.icon-envir-m:before {
content: "\e61a";
}
.icon-iceSnow-m:before {
content: "\e61b";
}
.icon-humidness-m:before {
content: "\e61c";
}
.icon-tem-m:before {
content: "\e61d";
}
.icon-satellite-m:before {
content: "\e61e";
}
.icon-forest-m:before {
content: "\e61f";
}
.icon-torrents-m:before {
content: "\e620";
}
.icon-typhoon-m:before {
content: "\e621";
}
.icon-light-m:before {
content: "\e622";
}
.icon-water-m:before {
content: "\e623";
}
.icon-wind-m:before {
content: "\e624";
}
.icon-situation-field-m:before {
content: "\e625";
}
.icon-important-process-w:before {
content: "\e626";
}
.icon-meeting-materials-w:before {
content: "\e627";
}
.icon-pattern-product-w:before {
content: "\e628";
}
.icon-be-onduty-w:before {
content: "\e629";
}
.icon-tem-f:before {
content: "\e62a";
}
.icon-torrents-f:before {
content: "\e62b";
}
.icon-rain-f:before {
content: "\e62c";
}
.icon-fog-f:before {
content: "\e62d";
}
.icon-envir-f:before {
content: "\e62e";
}
.icon-geology-disaster-f:before {
content: "\e62f";
}
.icon-grid-f:before {
content: "\e630";
}
.icon-areal-rainfall-f:before {
content: "\e631";
}
.icon-forest-fire-f:before {
content: "\e632";
}
.icon-drought-c:before {
content: "\e633";
}
.icon-metaphase-climate-c:before {
content: "\e634";
}
.icon-rain-c:before {
content: "\e635";
}
.icon-service-product-c:before {
content: "\e636";
}
.icon-tem-c:before {
content: "\e637";
}
.icon-gw:before {
content: "\e615";
}
.icon-bianzu:before {
content: "\e611";
}
.icon-location:before {
content: "\e610";
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论