lzq 2025-08-02 11:40:06 +08:00
parent f812ce5d43
commit 139ddd5a67
22 changed files with 426 additions and 78 deletions

6
.env
View File

@ -4,9 +4,9 @@ VITE_APP_NAME=垃圾回收监管平台
# 服务器基础地址 # 服务器基础地址
VITE_HTTP_SERVER_BASE_URL=/api VITE_HTTP_SERVER_BASE_URL=/api
VITE_WS_SERVER_BASE_URL=/ws VITE_WS_SERVER_BASE_URL=/ws
VITE_OSS_UPLOAD_BASE_URL=http://218.94.108.114:19000/iot VITE_OSS_UPLOAD_BASE_URL=http://localhost:9000/zsy
VITE_OSS_DOWNLOAD_BASE_URL=/api/file/oss/download VITE_OSS_DOWNLOAD_BASE_URL=/api/oss/download
VITE_OSS_BUCKET_NAME=iot VITE_OSS_BUCKET_NAME=zsy
# 接口超时时间 # 接口超时时间
VITE_HTTP_SERVER_TIMEOUT=10000 VITE_HTTP_SERVER_TIMEOUT=10000

17
package-lock.json generated
View File

@ -15,6 +15,7 @@
"axios": "1.7.4", "axios": "1.7.4",
"decimal.js": "^10.4.3", "decimal.js": "^10.4.3",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"flv.js": "^1.6.2",
"gridstack": "^12.0.0", "gridstack": "^12.0.0",
"logan-web": "^1.1.0", "logan-web": "^1.1.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
@ -3267,6 +3268,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/flv.js": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/flv.js/-/flv.js-1.6.2.tgz",
"integrity": "sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.8",
"webworkify-webpack": "^2.1.5"
}
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.9", "version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -6559,6 +6570,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz",
"integrity": "sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -16,6 +16,7 @@
"axios": "1.7.4", "axios": "1.7.4",
"decimal.js": "^10.4.3", "decimal.js": "^10.4.3",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"flv.js": "^1.6.2",
"gridstack": "^12.0.0", "gridstack": "^12.0.0",
"logan-web": "^1.1.0", "logan-web": "^1.1.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",

View File

@ -11,11 +11,12 @@ interface IconfontJson {
}[]; }[];
} }
export default function iconfontDts(outFile: string) { export default function iconfontProcess(outPath: string) {
return function (content: string) { return function (content: string) {
const json = JSON.parse(content) as IconfontJson const json = JSON.parse(content) as IconfontJson
const names = json.glyphs.map(glyph => glyph.font_class) const names = json.glyphs.map(glyph => glyph.font_class)
console.log('正在生成文件:', outFile) const dtsFile = outPath + '/iconfont.d.ts'
console.log('正在生成文件:', dtsFile)
const dts = `export {} const dts = `export {}
declare global { declare global {
@ -24,8 +25,13 @@ declare global {
} }
} }
` `
fs.writeFileSync(outFile, dts, {encoding: 'utf-8'}) fs.writeFileSync(dtsFile, dts, {encoding: 'utf-8'})
console.log('文件生成完成')
const tsFile = outPath + '/icons.ts'
const ts = `export default reactive([${'\n ' + names.map(name => `{name: '${name}'}`).join(',\n ') + '\n'}])`
console.log('正在生成文件:', tsFile)
fs.writeFileSync(tsFile, ts, {encoding: 'utf-8'})
console.log('文件生成完成')
} }
} }

View File

@ -1,5 +1,8 @@
import { get } from '@/common/utils/http-util.ts' import {
import { bucketName } from '@/common' get,
getFileUrl
} from '@/common/utils/http-util.ts'
import { bucketName, } from '@/common'
interface PresignedUrl extends Record<string, string | undefined> { interface PresignedUrl extends Record<string, string | undefined> {
bucketName?: string bucketName?: string
@ -8,9 +11,12 @@ interface PresignedUrl extends Record<string, string | undefined> {
export default { export default {
obtainPresignedUrl(filename: string) { obtainPresignedUrl(filename: string) {
return get<PresignedUrl>('/file/oss/obtain_presigned_url', {filename, bucketName}) return get<PresignedUrl>('/oss/obtain_presigned_url', {filename, bucketName})
}, },
download(objectName: string) { download(objectName: string) {
return get('/file/oss/download/' + objectName) return get('/oss/download/' + objectName)
}, },
fileUrl(filename: string) {
return getFileUrl(filename)
}
} }

View File

@ -14,7 +14,7 @@ export const appName = import.meta.env.VITE_APP_NAME
export const serverBaseUrl = import.meta.env.VITE_HTTP_SERVER_BASE_URL ?? '/' export const serverBaseUrl = import.meta.env.VITE_HTTP_SERVER_BASE_URL ?? '/'
export const uploadBaseUrl = import.meta.env.VITE_OSS_UPLOAD_BASE_URL ?? '/' export const uploadBaseUrl = import.meta.env.VITE_OSS_UPLOAD_BASE_URL ?? '/'
export const downloadBaseUrl = import.meta.env.VITE_OSS_DOWNLOAD_BASE_URL ?? '/' export const downloadBaseUrl = import.meta.env.VITE_OSS_DOWNLOAD_BASE_URL ?? '/'
export const bucketName = import.meta.env.VITE_OSS_BUCKET_NAME ?? 'iot' export const bucketName = import.meta.env.VITE_OSS_BUCKET_NAME ?? 'zsy'
/** /**
* Axios * Axios

View File

@ -1,4 +1,5 @@
import { import {
downloadBaseUrl,
serverBaseUrl, serverBaseUrl,
serverTimeout serverTimeout
} from '@/common' } from '@/common'
@ -6,6 +7,7 @@ import axios, { AxiosRequestConfig } from 'axios'
import * as qs from 'qs' import * as qs from 'qs'
import Toast from '@/components/toast' import Toast from '@/components/toast'
import { useAppUserStore } from '@/common/app/app-user-store.ts' import { useAppUserStore } from '@/common/app/app-user-store.ts'
import { throttle } from '@/common/utils/index.ts'
/** /**
* HTTP * HTTP
@ -41,24 +43,13 @@ const paramsSerializer = (params: any) => {
/** /**
* *
*
* @param code
* @param msg
* @param message
*/ */
function errHandler({code, msg, message}: R) { /* function errHandler(r?: R) {
switch (code) { Toast.error(r?.message ?? '操作失败')
case 450401: } */
console.log(msg, message) const errHandler = throttle(500, (r?: R) => {
break Toast.error(r?.message ?? '操作失败')
case 450403: })
console.log(msg, message)
break
default:
Toast.error(message ?? '操作失败')
}
}
/** /**
* axios * axios
*/ */
@ -98,7 +89,7 @@ httpUtil.interceptors.response.use(
return Promise.resolve(response) return Promise.resolve(response)
} }
if (response.data == null) { if (response.data == null) {
response.data = {code: 99999, msg: '无响应内容', message: '无响应内容', data: null, headers: response.headers} response.data = {code: 0, msg: '无响应内容', message: '无响应内容', data: null, headers: response.headers}
} }
response.data.headers = response.headers response.data.headers = response.headers
if (response.data.code === 0) { if (response.data.code === 0) {
@ -113,14 +104,14 @@ httpUtil.interceptors.response.use(
error.response.data = {...error.response.data, headers: error.response.headers} error.response.data = {...error.response.data, headers: error.response.headers}
} else if (error.request != null) { } else if (error.request != null) {
error.response = { error.response = {
data: {code: 99999, msg: '服务器未响', message: '服务器未响', data: null}, data: {code: 9999, msg: '网络异常', message: '网络异常', data: null},
} }
} else { } else {
error.response = { error.response = {
data: {code: 55555, msg: '请求发送失败', message: '请求发送失败', data: null}, data: {code: 5555, msg: '请求发送失败', message: '请求发送失败', data: null},
} }
} }
return Promise.reject(error) return Promise.reject(error.response.data)
}, },
) )
@ -132,11 +123,11 @@ httpUtil.interceptors.response.use(
* @param disposeErr -->true * @param disposeErr -->true
*/ */
export function get<T = any>(url: string, params?: any, disposeErr: boolean = true) { export function get<T = any>(url: string, params?: any, disposeErr: boolean = true) {
return httpUtil.get<R<T>>(url, {params, paramsSerializer}) return httpUtil.get<R<T>>(url, {params, paramsSerializer, responseType: 'json'})
.then(({data}) => data) .then(({data}) => data)
.catch(res => { .catch(res => {
if (disposeErr) errHandler(res.response.data) if (disposeErr) errHandler(res)
return Promise.reject(res.response.data) return Promise.reject(res)
}) })
} }
@ -149,11 +140,11 @@ export function get<T = any>(url: string, params?: any, disposeErr: boolean = tr
* @param disposeErr -->true * @param disposeErr -->true
*/ */
export function post<T>(url: string, body?: any, config?: AxiosConfig, disposeErr: boolean = true) { export function post<T>(url: string, body?: any, config?: AxiosConfig, disposeErr: boolean = true) {
return httpUtil.post<R<T>>(url, body, config) return httpUtil.post<R<T>>(url, body, {...config, responseType: 'json'})
.then(({data}) => data) .then(({data}) => data)
.catch(res => { .catch(res => {
if (disposeErr) errHandler(res.response.data) if (disposeErr) errHandler(res)
return Promise.reject(res.response.data) return Promise.reject(res)
}) })
} }
@ -173,12 +164,12 @@ export function postForm<T>(url: string, body: any, config?: AxiosConfig, dispos
'Content-Type': ContentType.FORM, 'Content-Type': ContentType.FORM,
}, },
params: config?.params, params: config?.params,
responseType: config?.responseType, responseType: config?.responseType ?? 'json',
}) })
.then(({data}) => data) .then(({data}) => data)
.catch(res => { .catch(res => {
if (disposeErr) errHandler(res.response.data) if (disposeErr) errHandler(res)
return Promise.reject(res.response.data) return Promise.reject(res)
}) })
} }
@ -198,12 +189,12 @@ export function postMltForm<T>(url: string, body: any, config?: AxiosConfig, dis
'Content-Type': ContentType.MLT_FORM, 'Content-Type': ContentType.MLT_FORM,
}, },
params: config?.params, params: config?.params,
responseType: config?.responseType, responseType: config?.responseType ?? 'json',
}) })
.then(({data}) => data) .then(({data}) => data)
.catch(res => { .catch(res => {
if (disposeErr) errHandler(res.response.data) if (disposeErr) errHandler(res)
return Promise.reject(res.response.data) return Promise.reject(res)
}) })
} }
@ -258,14 +249,21 @@ export function download(url: string, params?: any, disposeErr: boolean = true)
window.URL.revokeObjectURL(blobURL) window.URL.revokeObjectURL(blobURL)
}) })
.catch(res => { .catch(res => {
if (disposeErr) errHandler(res.response.data) if (disposeErr) errHandler(res)
return Promise.reject(res.response.data) return Promise.reject(res)
}) })
} }
export function getFileUrl(filename: string) {
const appUserStore = useAppUserStore()
return downloadBaseUrl + filename + '?authorization=' + appUserStore.token
}
export default { export default {
get, get,
post, post,
postForm, postForm,
postMltForm, postMltForm,
download,
getFileUrl,
} }

View File

@ -32,6 +32,7 @@ declare module 'vue' {
IxFormItem: typeof import('@idux/components/form')['IxFormItem'] IxFormItem: typeof import('@idux/components/form')['IxFormItem']
IxFormWrapper: typeof import('@idux/components/form')['IxFormWrapper'] IxFormWrapper: typeof import('@idux/components/form')['IxFormWrapper']
IxIcon: typeof import('@idux/components/icon')['IxIcon'] IxIcon: typeof import('@idux/components/icon')['IxIcon']
IxIconClose: typeof import('@idux/components/icon')['IxIconClose']
IxInput: typeof import('@idux/components/input')['IxInput'] IxInput: typeof import('@idux/components/input')['IxInput']
IxInputNumber: typeof import('@idux/components/input-number')['IxInputNumber'] IxInputNumber: typeof import('@idux/components/input-number')['IxInputNumber']
IxLayoutSiderTrigger: typeof import('@idux/components/layout')['IxLayoutSiderTrigger'] IxLayoutSiderTrigger: typeof import('@idux/components/layout')['IxLayoutSiderTrigger']
@ -43,12 +44,14 @@ declare module 'vue' {
IxModal: typeof import('@idux/components/modal')['IxModal'] IxModal: typeof import('@idux/components/modal')['IxModal']
IxPagination: typeof import('@idux/components/pagination')['IxPagination'] IxPagination: typeof import('@idux/components/pagination')['IxPagination']
IxPopconfirm: typeof import('@idux/components/popconfirm')['IxPopconfirm'] IxPopconfirm: typeof import('@idux/components/popconfirm')['IxPopconfirm']
IxPopover: typeof import('@idux/components/popover')['IxPopover']
IxProLayout: typeof import('@idux/pro/layout')['IxProLayout'] IxProLayout: typeof import('@idux/pro/layout')['IxProLayout']
IxPwdInput: typeof import('./../components/input/IxPwdInput.vue')['default'] IxPwdInput: typeof import('./../components/input/IxPwdInput.vue')['default']
IxRadio: typeof import('@idux/components/radio')['IxRadio'] IxRadio: typeof import('@idux/components/radio')['IxRadio']
IxRadioGroup: typeof import('@idux/components/radio')['IxRadioGroup'] IxRadioGroup: typeof import('@idux/components/radio')['IxRadioGroup']
IxRow: typeof import('@idux/components/grid')['IxRow'] IxRow: typeof import('@idux/components/grid')['IxRow']
IxSelect: typeof import('@idux/components/select')['IxSelect'] IxSelect: typeof import('@idux/components/select')['IxSelect']
IxSelector: typeof import('@idux/components/selector')['IxSelector']
IxSpace: typeof import('@idux/components/space')['IxSpace'] IxSpace: typeof import('@idux/components/space')['IxSpace']
IxTable: typeof import('@idux/components/table')['IxTable'] IxTable: typeof import('@idux/components/table')['IxTable']
IxTabs: typeof import('@idux/components/tabs')['IxTabs'] IxTabs: typeof import('@idux/components/tabs')['IxTabs']

View File

@ -114,21 +114,20 @@ const columns: TableColumn[] = [
const pagination = reactive<TablePagination>({ const pagination = reactive<TablePagination>({
pageIndex: 1, pageIndex: 1,
pageSize: 10, pageSize: 3,
total: 100, total: 100,
size: 'sm', size: 'sm',
showTotal: true, showTotal: true,
onChange(pageIndex: number, pageSize: number) { onChange(pageIndex: number, pageSize: number) {
console.log('------', pageIndex, pageSize)
pagination.pageIndex = pageIndex pagination.pageIndex = pageIndex
pagination.pageSize = pageSize pagination.pageSize = pageSize
searchHandler()
} }
}) })
const tableSpin = ref(false) const tableSpin = ref(false)
function searchHandler() { function searchHandler() {
console.log('------', formGroup.getValue())
tableSpin.value = true tableSpin.value = true
DisposeRecodeApi.paging({ DisposeRecodeApi.paging({
...formGroup.getValue(), ...formGroup.getValue(),
@ -218,7 +217,7 @@ onMounted(() => {
.dispose-recode { .dispose-recode {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 150%; height: 100%;
width: 100%; width: 100%;
.title { .title {

View File

@ -20,13 +20,9 @@ const data = reactive<DisposeRecodeTypes.DisposeRecodeData>({
}) })
onMounted(() => { onMounted(() => {
console.log(ins)
let container = props.container let container = props.container
if (container) { if (container) {
container.style.position = 'relative' container.style.position = 'relative'
for (let child of container.children) {
console.log(child)
}
} }
}) })

View File

@ -37,10 +37,7 @@ import {
type ProLayoutType type ProLayoutType
} from '@idux/pro/layout' } from '@idux/pro/layout'
import Strings from '@/common/utils/strings.ts' import Strings from '@/common/utils/strings.ts'
import { import { appName } from '@/common'
appName,
downloadBaseUrl
} from '@/common'
import { useAppSettingStore } from '@/common/app/app-setting-store.ts' import { useAppSettingStore } from '@/common/app/app-setting-store.ts'
import colls from '@/common/utils/colls.ts' import colls from '@/common/utils/colls.ts'
import { import {
@ -49,10 +46,11 @@ import {
} from '@/common/app/contants.ts' } from '@/common/app/contants.ts'
import Nav from '@/common/router/nav.ts' import Nav from '@/common/router/nav.ts'
import Iconfont from '@/components/iconfont/Iconfont.vue' import Iconfont from '@/components/iconfont/Iconfont.vue'
import AppApi from '@/common/app/app-api.ts'
const appSettingStore = useAppSettingStore() const appSettingStore = useAppSettingStore()
const logoImage = computed(() => { const logoImage = computed(() => {
return Strings.isBlank(appSettingStore.logo) ? defaultLogo : downloadBaseUrl + appSettingStore.logo! return Strings.isBlank(appSettingStore.logo) ? defaultLogo : AppApi.fileUrl(appSettingStore.logo!)
}) })
const logo = { const logo = {
image: logoImage.value, image: logoImage.value,
@ -131,7 +129,7 @@ function openPageHandler(options: MenuClickOptions) {
.page-change-enter-active, .page-change-enter-active,
.page-change-leave-active { .page-change-leave-active {
transition: all .3s ease-in; transition: opacity .3s ease-in, transform .3s ease-in;
} }
.page-change-enter-from { .page-change-enter-from {

View File

@ -33,11 +33,11 @@ import TabList from '@/pages/main-zone/tab-list/TabList.vue'
import Strings from '@/common/utils/strings.ts' import Strings from '@/common/utils/strings.ts'
import { useAppSettingStore } from '@/common/app/app-setting-store.ts' import { useAppSettingStore } from '@/common/app/app-setting-store.ts'
import UserPanel from '@/pages/main-zone/header-bar/UserPanel.vue' import UserPanel from '@/pages/main-zone/header-bar/UserPanel.vue'
import { downloadBaseUrl } from '@/common' import AppApi from '@/common/app/app-api.ts'
const appSettingStore = useAppSettingStore() const appSettingStore = useAppSettingStore()
const logo = computed(() => { const logo = computed(() => {
return Strings.isBlank(appSettingStore.logo) ? defaultLogo : downloadBaseUrl + appSettingStore.logo! return Strings.isBlank(appSettingStore.logo) ? defaultLogo : AppApi.fileUrl(appSettingStore.logo!)
}) })
const menuPanel = ref<InstanceType<typeof MenuPanel>>() const menuPanel = ref<InstanceType<typeof MenuPanel>>()

View File

@ -48,10 +48,10 @@ import ModifyPwdForm from '@/pages/main-zone/header-bar/ModifyPwdForm.vue'
import UserInfo from '@/pages/main-zone/header-bar/UserInfo.vue' import UserInfo from '@/pages/main-zone/header-bar/UserInfo.vue'
import { VKey } from '@idux/cdk' import { VKey } from '@idux/cdk'
import Nav from '@/common/router/nav.ts' import Nav from '@/common/router/nav.ts'
import { downloadBaseUrl } from '@/common'
import MenuPanelApi from '@/pages/main-zone/menu-panel/menu-panel-api.ts' import MenuPanelApi from '@/pages/main-zone/menu-panel/menu-panel-api.ts'
import Toast from '@/components/toast' import Toast from '@/components/toast'
import { reloadUserInfo } from '@/common/app' import { reloadUserInfo } from '@/common/app'
import AppApi from '@/common/app/app-api.ts'
const appSettingStore = useAppSettingStore() const appSettingStore = useAppSettingStore()
const appUserStore = useAppUserStore() const appUserStore = useAppUserStore()
@ -66,7 +66,7 @@ const tabsDtaSource = [
] ]
const avatar = computed(() => { const avatar = computed(() => {
return Strings.isBlank(appUserStore.avatar) ? defaultAvatar : downloadBaseUrl + appUserStore.avatar! return Strings.isBlank(appUserStore.avatar) ? defaultAvatar : AppApi.fileUrl(appUserStore.avatar!)
}) })
const tenantName = computed(() => { const tenantName = computed(() => {
return appUserStore.tenantName return appUserStore.tenantName

View File

@ -35,7 +35,7 @@ const leftFuns = computed<PageTypes.Fun[]>(() => {
menuForm.value!.reset() menuForm.value!.reset()
}) })
.catch(_ => { .catch(_ => {
Toast.success('添加失败') Toast.error('添加失败')
}) })
.finally(() => { .finally(() => {
Toast.close(toastId) Toast.close(toastId)

View File

@ -58,6 +58,20 @@
</IxFormItem> </IxFormItem>
</IxCol> </IxCol>
</IxRow> </IxRow>
<IxRow>
<IxCol :span="6">
<IxFormItem
:gutter="[0, 10]"
label="图标">
<IxPopover closable header="图标列表" placement="bottom" trigger="click">
<IxInput control="icon"/>
<template #content>
<IxTable v-model:selectedRowKeys="selectedRowKeys" :columns="columns" :dataSource="iconTableDataSource" :pagination="pagination" get-key="name"/>
</template>
</IxPopover>
</IxFormItem>
</IxCol>
</IxRow>
</IxForm> </IxForm>
</template> </template>
@ -75,7 +89,61 @@ import { TreeSelectNode } from '@idux/components/tree-select/src/types'
import { SelectData } from '@idux/components/select/src/types' import { SelectData } from '@idux/components/select/src/types'
import MenuApi from '@/pages/sys/menus/menu-api.ts' import MenuApi from '@/pages/sys/menus/menu-api.ts'
import Colls from '@/common/utils/colls.ts' import Colls from '@/common/utils/colls.ts'
import icons from '@/components/iconfont/icons.ts'
import {
TableColumn,
TableColumnSelectable
} from '@idux/components/table'
import Iconfont from '@/components/iconfont/Iconfont.vue'
import { TablePagination } from '@idux/components/table/src/types'
interface IconData {
name: string
}
const selectedRowKeys = ref<string[]>([])
const iconTableDataSource = ref<IconData[]>(icons.filter((_, i) => {
return i >= 0 && i < 5
}))
const selectableColumn = reactive<TableColumnSelectable<IconData>>({
type: 'selectable',
align: 'center',
multiple: false,
showIndex: false,
trigger: 'click',
onChange: (selectedKeys) => {
menuForm.get('icon')?.setValue(selectedKeys[0] as string)
},
})
const columns: TableColumn<IconData>[] = [
selectableColumn,
{
title: '图标',
dataKey: 'name',
customCell: ({record}: { record: IconData }) => {
return h(Iconfont, {name: record.name})
}
},
{
title: '名称',
dataKey: 'name',
},
]
const pagination = reactive<TablePagination>({
pageIndex: 1,
pageSize: 5,
total: icons.length,
size: 'sm',
showTotal: true,
onChange(pageIndex: number, pageSize: number) {
pagination.pageIndex = pageIndex
pagination.pageSize = pageSize
iconTableDataSource.value = icons.filter((_, i) => {
return i >= (pageIndex - 1) * pageSize && i < pageIndex * pageSize
})
}
})
const props = withDefaults(defineProps<{ status?: 'create' | 'view' | 'edit' }>(), const props = withDefaults(defineProps<{ status?: 'create' | 'view' | 'edit' }>(),
{ {
status: 'create', status: 'create',
@ -89,6 +157,7 @@ const menuForm = useFormGroup<MenuTypes.MenuForm>({
sort: [ 0 ], sort: [ 0 ],
routeName: [ '' ], routeName: [ '' ],
sn: [ undefined ], sn: [ undefined ],
icon: [ undefined ],
}) })
const menuCategoryCtrl = menuForm.get('menuCategory') const menuCategoryCtrl = menuForm.get('menuCategory')
const routeNameCtrl = menuForm.get('routeName') const routeNameCtrl = menuForm.get('routeName')
@ -140,6 +209,9 @@ defineExpose({
}, },
setValue(data: MenuTypes.MenuForm) { setValue(data: MenuTypes.MenuForm) {
menuForm.setValue(data) menuForm.setValue(data)
if (data.icon != null) {
selectedRowKeys.value = [ data.icon ]
}
}, },
reset() { reset() {
menuForm.reset() menuForm.reset()

View File

@ -14,7 +14,7 @@ declare global {
// 菜单名称 // 菜单名称
title: string title: string
// 图标 // 图标
icon: string icon?: string
// 层级; >= 1 // 层级; >= 1
tier: number tier: number
// 排序 // 排序
@ -28,7 +28,7 @@ declare global {
} }
type SearchForm = Partial<Pick<SysMenu, 'title' | 'routeName'>> type SearchForm = Partial<Pick<SysMenu, 'title' | 'routeName'>>
type MenuForm = Pick<SysMenu, 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName' | 'sn'> type MenuForm = Pick<SysMenu, 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName' | 'sn' | 'icon'>
type AddForm = Pick<SysMenu, 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName' | 'sn'> type AddForm = Pick<SysMenu, 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName' | 'sn'>
type ModifyForm = Pick<SysMenu, 'id' | 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName'> type ModifyForm = Pick<SysMenu, 'id' | 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName'>
} }

View File

@ -5,7 +5,7 @@
<IxInput control="nickname" placeholder="请输入用户昵称"></IxInput> <IxInput control="nickname" placeholder="请输入用户昵称"></IxInput>
</IxFormItem> </IxFormItem>
<IxFormItem :gutter="[0, 10]" label="账号"> <IxFormItem :gutter="[0, 10]" label="账号">
<IxInput control="routeName" placeholder="请输入账号"></IxInput> <IxInput control="username" placeholder="请输入账号"></IxInput>
</IxFormItem> </IxFormItem>
<button style="display: none" type="submit"/> <button style="display: none" type="submit"/>
</IxForm> </IxForm>
@ -28,7 +28,7 @@
</template> </template>
<template #avatar="{record}"> <template #avatar="{record}">
<img v-if="!record.avatar" :src="defaultAvatar" alt="头像" style="width: 2rem"> <img v-if="!record.avatar" :src="defaultAvatar" alt="头像" style="width: 2rem">
<img v-else :src="record.avatar" alt="头像" style="width: 2rem"> <img v-else :src="AppApi.fileUrl(record.avatar)" alt="头像" style="width: 2rem">
</template> </template>
</IxTable> </IxTable>
<IxRow justify="end"> <IxRow justify="end">
@ -56,6 +56,7 @@ import Toast from '@/components/toast'
import UserApi from '@/pages/sys/user/user-api.ts' import UserApi from '@/pages/sys/user/user-api.ts'
import defaultAvatar from '@/assets/images/avatar.png' import defaultAvatar from '@/assets/images/avatar.png'
import { useUserDetailStore } from '@/pages/sys/user/user-detail/user-detail-store.ts' import { useUserDetailStore } from '@/pages/sys/user/user-detail/user-detail-store.ts'
import AppApi from '@/common/app/app-api.ts'
const searchForm = useFormGroup<UserTypes.SearchForm>({ const searchForm = useFormGroup<UserTypes.SearchForm>({
nickname: [ undefined ], nickname: [ undefined ],

View File

@ -84,7 +84,7 @@
<IxForm :control="searchForm" :control-col="{span:18}" :label-col="{span:6}" <IxForm :control="searchForm" :control-col="{span:18}" :label-col="{span:6}"
label-align="end" label-align="end"
layout="horizontal" layout="horizontal"
@submit="searchHandler"> @submit.prevent="searchHandler">
<IxRow> <IxRow>
<IxCol :span="6"> <IxCol :span="6">
<IxFormItem :gutter="[0, 10]" align="center" label="角色编码"> <IxFormItem :gutter="[0, 10]" align="center" label="角色编码">
@ -161,6 +161,7 @@ const pagination = reactive<G.Pagination>({
size: 100, size: 100,
total: 0, total: 0,
}) })
const searchForm = useFormGroup<RoleTypes.SearchForm>({ const searchForm = useFormGroup<RoleTypes.SearchForm>({
roleCode: [ undefined ], roleCode: [ undefined ],
roleName: [ undefined ] roleName: [ undefined ]
@ -243,7 +244,7 @@ const columns = computed<TableColumn<RoleTypes.SysRole>[]>(() => ([
const genderDataSource = computed<SelectData[]>(() => Object.entries(Gender).map(([ key, label ]) => ({key, label}))) const genderDataSource = computed<SelectData[]>(() => Object.entries(Gender).map(([ key, label ]) => ({key, label})))
const files = ref() const files = ref<UploadFile []>()
function updateFormStatus() { function updateFormStatus() {
@ -262,6 +263,13 @@ defineExpose({
}, },
setValue(data: UserTypes.SysUser) { setValue(data: UserTypes.SysUser) {
formGroup.setValue(data) formGroup.setValue(data)
if (data.avatar != null) {
files.value = [ {
key: data.avatar,
name: data.avatar,
thumbUrl: AppApi.fileUrl(data.avatar),
} ]
}
if (data.roles != null) { if (data.roles != null) {
selectedRowKeys.value = data.roles.map(it => it.id as string) selectedRowKeys.value = data.roles.map(it => it.id as string)
} }

View File

@ -6,10 +6,13 @@ import { TablePagination } from '@idux/components/table/src/types'
import CreateTsp from '@/pages/tsp/create-tsp.vue' import CreateTsp from '@/pages/tsp/create-tsp.vue'
import TspApi from '@/pages/tsp/tsp-api.ts' import TspApi from '@/pages/tsp/tsp-api.ts'
import { ref } from 'vue' import { ref } from 'vue'
import VideoPanel from '@/pages/tsp/VideoPanel.vue'
import Toast from '@/components/toast'
const createTsp = ref<InstanceType<typeof CreateTsp> | null>(null) const createTsp = ref<InstanceType<typeof CreateTsp> | null>(null)
const videoPanel = ref<InstanceType<typeof VideoPanel> | null>(null)
const dataSource: SelectData[] = [ const dataSource: SelectData[] = [
{key: '', label: '所有'}, {key: '', label: '全部状态'},
{key: 'ZhengChang', label: '正常运营'}, {key: 'ZhengChang', label: '正常运营'},
{key: 'FenZhengChang', label: '非正常运营'}, {key: 'FenZhengChang', label: '非正常运营'},
] ]
@ -21,9 +24,9 @@ const pagination = reactive<TablePagination>({
size: 'sm', size: 'sm',
showTotal: true, showTotal: true,
onChange(pageIndex: number, pageSize: number) { onChange(pageIndex: number, pageSize: number) {
console.log('------', pageIndex, pageSize)
pagination.pageIndex = pageIndex pagination.pageIndex = pageIndex
pagination.pageSize = pageSize pagination.pageSize = pageSize
searchHandler()
} }
}) })
@ -89,6 +92,14 @@ function searchHandler() {
}) })
} }
function playVideo(record: TspTypes.TspData) {
if (record.videoUrl == null) {
Toast.error('该收纳点未配置视频监控')
return
}
videoPanel.value?.open(record.videoUrl, record.pointName)
}
onMounted(() => { onMounted(() => {
searchHandler() searchHandler()
TspApi.statistics() TspApi.statistics()
@ -154,8 +165,8 @@ onMounted(() => {
<Iconfont name="mapmarker" wrapper/> <Iconfont name="mapmarker" wrapper/>
<span>{{ record.pointName }}</span> <span>{{ record.pointName }}</span>
</template> </template>
<template #action> <template #action="{record}">
<IxButton class="video-btn" icon="eye" mode="text">查看监</IxButton> <IxButton class="video-btn" icon="eye" mode="text" @click="playVideo(record)"></IxButton>
</template> </template>
<template #status="{record}"> <template #status="{record}">
<IxTag v-if="record.status === 'ZhengChang'" status="success">{{ record.statusTxt }}</IxTag> <IxTag v-if="record.status === 'ZhengChang'" status="success">{{ record.statusTxt }}</IxTag>
@ -164,11 +175,13 @@ onMounted(() => {
</IxTable> </IxTable>
</IxCard> </IxCard>
<CreateTsp ref="createTsp"/> <CreateTsp ref="createTsp"/>
<VideoPanel ref="videoPanel"/>
</div> </div>
</template> </template>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.tsp { .tsp {
position relative
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;

View File

@ -0,0 +1,224 @@
<script lang="ts" setup>
import FlvJs from 'flv.js'
import { nanoid } from 'nanoid'
import Toast from '@/components/toast'
const show = ref(false)
const fullscreen = ref(false)
// 0-->1-->2-->
const status = ref(0)
const video = ref<HTMLVideoElement | null>(null)
const containerElement = ref<HTMLElement | null>(null)
let videoUrl: string = ''
const header = ref<string>('视频监控')
// http://localhost:8888/live?port=1935&app=live&stream=stream1
// ffmpeg -re -i "C:\Users\24955\Downloads\1.mp4" -c:v libx264 -c:a aac -f flv rtmp://localhost:1935/live/stream1
const id = nanoid()
let flvPlayer: FlvJs.Player | null = null
/* const logListener = (type: any, str: any) => {
console.log(type, str)
} */
function toggleScreen() {
if (fullscreen.value) {
fullscreen.value = false
document.exitFullscreen()
} else {
fullscreen.value = true
containerElement.value!.requestFullscreen()
}
}
function flvErrorHandler(type: string, message: string) {
switch (type) {
case FlvJs.ErrorTypes.NETWORK_ERROR:
console.log(message)
Toast.error('网络异常,播放失败')
break
case FlvJs.ErrorTypes.MEDIA_ERROR:
console.log(message)
Toast.error('视频格式错误,播放失败')
break
default:
console.log(message)
Toast.error('视频播放失败')
}
}
function createPlayer() {
if (FlvJs.isSupported()) {
flvPlayer = FlvJs.createPlayer({
type: 'flv',
url: videoUrl
})
flvPlayer.on(FlvJs.Events.ERROR, flvErrorHandler)
flvPlayer.attachMediaElement(video.value!)
flvPlayer?.load()
play()
} else {
Toast.error('当前浏览器不支持视频播放')
}
}
function play() {
flvPlayer?.play()
status.value = 1
}
function destroy() {
// FlvJs.LoggingControl.removeLogListener(logListener)
flvPlayer?.off(FlvJs.Events.ERROR, flvErrorHandler)
flvPlayer?.pause()
flvPlayer?.unload()
flvPlayer?.detachMediaElement()
flvPlayer?.destroy()
flvPlayer = null
status.value = 0
}
function reload() {
destroy()
createPlayer()
play()
}
function pauseOrPlay() {
if (status.value === 1) {
flvPlayer?.pause()
status.value = 2
} else {
play()
}
}
function closeHandler() {
destroy()
containerElement.value!.style!.width = '0'
containerElement.value!.style!.height = '0'
videoUrl = ''
if (fullscreen.value) toggleScreen()
show.value = false
}
function handleFullscreenChange() {
fullscreen.value = document.fullscreenElement === containerElement.value
}
onMounted(() => {
document.addEventListener('fullscreenchange', handleFullscreenChange)
})
onBeforeUnmount(() => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
})
defineExpose({
open(url: string, title?: string) {
status.value = 0
videoUrl = url
header.value = title ?? '视频监控'
show.value = true
containerElement.value!.style!.width = '100%'
containerElement.value!.style!.height = '100%'
}
})
</script>
<template>
<div :id="id" ref="containerElement" class="ix_modal-container">
<IxModal v-model:visible="show" :container="containerElement!"
:header="header" :mask="false"
:on-after-open="createPlayer"
:on-before-close="closeHandler"
type="default" width="100%">
<video ref="video" class="video-element" controls></video>
<template #footer>
<IxTooltip placement="top" title="刷新">
<IxButton icon="sync" @click="reload"/>
</IxTooltip>
<IxTooltip :title="status === 1?'暂停':'播放'" placement="top">
<IxButton :icon="status === 1?'pause':'play'" mode="primary" @click="pauseOrPlay"/>
</IxTooltip>
<IxTooltip :title="fullscreen?'退出全屏':'全屏'" placement="top">
<IxButton :icon="fullscreen?'fullscreen-exit':'fullscreen'" @click="toggleScreen"/>
</IxTooltip>
</template>
</IxModal>
</div>
</template>
<style lang="stylus" scoped>
.ix_modal-container {
position absolute
top 0
left 0
:deep(.ix-modal) {
width 100%
height 100%
.ix-modal-body {
width 100%
overflow hidden
border-top: 1px dashed #E1E5EB;
border-bottom: 1px dashed #E1E5EB;
.video-element {
width 100%
height 100%
}
}
.ix-modal-footer {
justify-content center
}
}
}
/* 隐藏全屏按钮 */
video::-webkit-media-controls-fullscreen-button {
display: none;
}
/* 隐藏播放按钮 */
video::-webkit-media-controls-play-button {
display: none;
}
/* 隐藏进度条 */
video::-webkit-media-controls-timeline {
display: none;
}
/* 隐藏当前时间显示 */
video::-webkit-media-controls-current-time-display {
display: none;
}
/* 隐藏剩余时间显示 */
video::-webkit-media-controls-time-remaining-display {
display: none;
}
/* 隐藏音量按钮 */
video::-webkit-media-controls-mute-button {
display: none;
}
/* 隐藏音量控制条 */
video::-webkit-media-controls-volume-slider {
display: none;
}
/* 隐藏所有控件 */
video::-webkit-media-controls-enclosure {
display: none;
}
</style>

View File

@ -8,6 +8,7 @@ declare global {
microdistrict: string microdistrict: string
propertyManagement: string propertyManagement: string
status: 'ZhengChang' | 'FenZhengChang' status: 'ZhengChang' | 'FenZhengChang'
videoUrl?: string
} }
interface AddParam { interface AddParam {

View File

@ -16,7 +16,7 @@ import processHtml from './plugin/html-process'
import zipDist from './plugin/zip-dist' import zipDist from './plugin/zip-dist'
import { viteStaticCopy } from 'vite-plugin-static-copy' import { viteStaticCopy } from 'vite-plugin-static-copy'
import { fileWatcher } from './plugin/file-watcher' import { fileWatcher } from './plugin/file-watcher'
import iconfontDts from './plugin/iconfont-dts' import iconfontProcess from './plugin/iconfont-process'
let viteConfig: UserConfigFnObject = configEnv => { let viteConfig: UserConfigFnObject = configEnv => {
const env = loadEnv(configEnv.mode, process.cwd(), '') const env = loadEnv(configEnv.mode, process.cwd(), '')
@ -51,7 +51,7 @@ let viteConfig: UserConfigFnObject = configEnv => {
Icons(), Icons(),
fileWatcher({ fileWatcher({
file: './public/iconfont/ali/iconfont.json', file: './public/iconfont/ali/iconfont.json',
fn: iconfontDts('./src/components/iconfont/iconfont.d.ts'), fn: iconfontProcess('./src/components/iconfont'),
}), }),
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [
@ -91,6 +91,11 @@ let viteConfig: UserConfigFnObject = configEnv => {
target: env.VITE_HTTP_PROXY_TARGET, target: env.VITE_HTTP_PROXY_TARGET,
rewrite: path => env.VITE_HTTP_SERVER_BASE_URL == null || env.VITE_HTTP_SERVER_BASE_URL == '/' ? path : path.replace(new RegExp(env.VITE_HTTP_SERVER_BASE_URL), ''), rewrite: path => env.VITE_HTTP_SERVER_BASE_URL == null || env.VITE_HTTP_SERVER_BASE_URL == '/' ? path : path.replace(new RegExp(env.VITE_HTTP_SERVER_BASE_URL), ''),
} as ProxyOptions, } as ProxyOptions,
[env.VITE_OSS_UPLOAD_BASE_URL]: {
proxyTimeout: 10000,
target: 'http://localhost:9000',
rewrite: path => env.VITE_OSS_UPLOAD_BASE_URL == null || env.VITE_OSS_UPLOAD_BASE_URL == '/' ? path : path.replace(new RegExp(env.VITE_OSS_UPLOAD_BASE_URL), ''),
} as ProxyOptions,
[env.VITE_WS_SERVER_BASE_URL]: { [env.VITE_WS_SERVER_BASE_URL]: {
ws: true, ws: true,
target: env.VITE_WS_PROXY_TARGET, target: env.VITE_WS_PROXY_TARGET,