上传照片

master
lzq 2025-08-21 19:16:11 +08:00
parent 9f33b86a18
commit 15981273b0
17 changed files with 539 additions and 60 deletions

View File

@ -3,33 +3,72 @@ import fs from 'fs'
import path from 'path'
interface FileWatcherOptions {
// 目标可以是文件路径或文件夹路径
file: string;
fn: (content: string) => void;
// 回调函数,接收变化的文件路径、内容(如果有)和变化类型
fn: (filePath: string, content?: string, eventType?: 'add' | 'change' | 'unlink') => void;
// 防抖延迟默认300ms
delay?: number;
}
export function fileWatcher(options: FileWatcherOptions): Plugin {
const {file, fn, delay = 300} = options
const targetPath = path.resolve(file)
let debounceTimer: NodeJS.Timeout | null = null
// 检查目标是否为文件夹
const isDir = fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()
console.log('正在监听文件:' + targetPath)
return {
name: 'file-watcher-plugin',
configureServer(server) {
server.watcher.add(path.resolve(file))
server.watcher.on('change', (filePath: string) => {
if (filePath === path.resolve(file)) {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
try {
const content = fs.readFileSync(filePath, 'utf-8')
fn(content)
} catch (error) {
console.error('文件变化处理失败:', error)
}
}, delay)
configureServer(server) {
// 添加要监听的目标(文件或文件夹)
server.watcher.add(targetPath)
// 处理文件/文件夹变化的通用函数
const handleChange = (filePath: string, eventType: 'add' | 'change' | 'unlink') => {
if (isDir && !filePath.startsWith(targetPath)) {
return
}
if (!isDir && filePath !== targetPath) {
return
}
// 防抖处理
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(async () => {
try {
let content: string | undefined
if (!isDir) {
content = fs.readFileSync(filePath, 'utf-8')
}
fn(filePath, content, eventType)
} catch (error) {
console.error('处理文件变化时出错:', error)
}
}, delay)
}
if (isDir) {
// 监听文件新增
server.watcher.on('add', (filePath) => {
handleChange(filePath, 'add')
})
// 监听文件删除
server.watcher.on('unlink', (filePath) => {
handleChange(filePath, 'unlink')
})
}
// 监听文件修改
server.watcher.on('change', (filePath) => {
handleChange(filePath, 'change')
})
},
}
}

View File

@ -12,8 +12,9 @@ interface IconfontJson {
}
export default function iconfontProcess(outPath: string) {
return function (content: string) {
const json = JSON.parse(content) as IconfontJson
return function (filePath: string, content?: string, eventType?: 'add' | 'change' | 'unlink') {
console.log(filePath, content, eventType)
const json = JSON.parse(content!) as IconfontJson
const names = json.glyphs.map(glyph => glyph.font_class)
const dtsFile = outPath + '/iconfont.d.ts'
console.log('正在生成文件:', dtsFile)

View File

@ -48,7 +48,7 @@ const paramsSerializer = (params: any) => {
Toast.error(r?.message ?? '操作失败')
} */
const errHandler = throttle(500, (r?: R) => {
Toast.error(r?.message ?? '操作失败')
Toast.error(r?.message ?? '服务器错误')
})
/**
* axios

View File

@ -136,10 +136,26 @@ export function pretty(date: DateTime<true | false> | Date | number | string) {
return '现在'
}
export function endOfMonth(date?: DateTime<true | false>) {
if (date == null) {
return date = now()
}
return date.endOf('month')
}
export function beginOfMonth(date?: DateTime<true | false>) {
if (date == null) {
return date = now()
}
return date.startOf('month')
}
export default {
now,
parse,
format,
pretty,
FMT,
endOfMonth,
beginOfMonth,
}

View File

@ -82,21 +82,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type {
Component,
ComponentPublicInstance,
ComputedRef,
DirectiveBinding,
ExtractDefaultPropTypes,
ExtractPropTypes,
ExtractPublicPropTypes,
InjectionKey,
PropType,
Ref,
MaybeRef,
MaybeRefOrGetter,
VNode,
WritableComputedRef
} from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -25,6 +25,7 @@ declare module 'vue' {
IxFormItem: typeof import('@idux/components/form')['IxFormItem']
IxFormWrapper: typeof import('@idux/components/form')['IxFormWrapper']
IxIcon: typeof import('@idux/components/icon')['IxIcon']
IxImage: typeof import('@idux/components/image')['IxImage']
IxInput: typeof import('@idux/components/input')['IxInput']
IxInputNumber: typeof import('@idux/components/input-number')['IxInputNumber']
IxLayoutSiderTrigger: typeof import('@idux/components/layout')['IxLayoutSiderTrigger']
@ -44,6 +45,7 @@ declare module 'vue' {
IxTable: typeof import('@idux/components/table')['IxTable']
IxTabs: typeof import('@idux/components/tabs')['IxTabs']
IxTag: typeof import('@idux/components/tag')['IxTag']
IxTagGroup: typeof import('@idux/components/tag')['IxTagGroup']
IxTooltip: typeof import('@idux/components/tooltip')['IxTooltip']
IxTreeSelect: typeof import('@idux/components/tree-select')['IxTreeSelect']
IxUpload: typeof import('@idux/components/upload')['IxUpload']

View File

@ -63,11 +63,18 @@ defineExpose({
<p class="detail-item img-card"><span class="img-title">出出场车头照</span><img :src="data.outFrontPhoto" alt="出场前"></p>
<p class="detail-item img-card"><span class="img-title">出场车身照</span><img :src="data.outBodyPhoto" alt="出场后"></p>
</div>
<div class="dispose-recode-detail-img-title">装车照片</div>
<div v-if="data.tspPhotos == null || data.tspPhotos.length === 0">
<IxEmpty/>
</div>
<div v-else class="dispose-recode-detail-img">
<p v-for="tspPhoto in data.tspPhotos" class="detail-item img-card"><span class="img-title"></span><img :src="tspPhoto" alt="装车照片"></p>
</div>
<template #footer="{ cancel:_, ok }">
<IxButton mode="primary" @click="ok"></IxButton>
</template>
</IxModal>
<IxButton mode="primary" @click="ok"></IxButton>
</template>
</IxModal>
</div>
</template>

View File

@ -18,6 +18,7 @@ declare global {
inBodyPhoto: string
outFrontPhoto: string
outBodyPhoto: string
tspPhotos?: string[]
}
interface SearchParam {

View File

@ -27,12 +27,12 @@
</IxButtonGroup>
</template>
</IxTable>
<IxRow justify="end">
<!--<IxRow justify="end">
<IxPagination v-model:pageIndex="pagination.current" v-model:pageSize="pagination.size" :total="pagination.total"
class="pagination"
show-quick-jumper
show-size-changer show-total @change="pagingChangeHandler"/>
</IxRow>
</IxRow>-->
<IxModal v-model:visible="visibleDialog" title="删除提醒" type="confirm" @ok="confirmDelHandler">
<p>是否删除已选数据</p>
</IxModal>
@ -59,6 +59,7 @@ import MenuApi from '@/pages/sys/menus/menu-api.ts'
import { TreeSelectNode } from '@idux/components/tree-select/src/types'
import Toast from '@/components/toast'
import { useMenuDetailStore } from '@/pages/sys/menus/menu-detail/menu-detail-store.ts'
import Iconfont from '@/components/iconfont/Iconfont.vue'
const searchForm = useFormGroup<MenuTypes.SearchForm>({
title: [ '' ],
@ -67,11 +68,6 @@ const searchForm = useFormGroup<MenuTypes.SearchForm>({
const visibleDialog = ref(false)
const tableSpin = ref(false)
const selectedRowKeys = ref<VKey[]>([])
const pagination = reactive<G.Pagination>({
current: 1,
size: 100,
total: 0,
})
const datasource = ref<TreeSelectNode[]>()
const columns: TableColumn<MenuTypes.SysMenu>[] = [
@ -96,6 +92,9 @@ const columns: TableColumn<MenuTypes.SysMenu>[] = [
{
title: '图标',
dataKey: 'icon',
customCell({value}: { value: string; record: MenuTypes.SysMenu; rowIndex: number }) {
return h(Iconfont, {name: value as IconfontTypes.name})
}
},
{
title: '路径',
@ -180,12 +179,9 @@ function searchHandler() {
function pageList() {
tableSpin.value = true
MenuApi.pageList(searchForm.getValue(), {...pagination})
MenuApi.list(searchForm.getValue())
.then(res => {
pagination.current = res.data.current
pagination.size = res.data.size
pagination.total = res.data.total
datasource.value = colls.toTree(res.data.records.map(it => {
datasource.value = colls.toTree(res.data.map(it => {
return {
key: it.id,
...it
@ -197,10 +193,6 @@ function pageList() {
})
}
function pagingChangeHandler() {
pageList()
}
function del(...recode: { id: string }[]) {
const toastId = Toast.loading('正在提交')
MenuApi.del(recode.map(it => it.id))

View File

@ -13,8 +13,8 @@ export default {
paging(data: MenuTypes.SearchForm & G.PageParam) {
return get<G.PageResult<MenuTypes.SysMenu>>('/sys_menu/page_list', data)
},
list(pid: string | null = null) {
return get<MenuTypes.SysMenu[]>('/sys_menu/list', {pid: pid})
list(data?: MenuTypes.SearchForm | null) {
return get<MenuTypes.SysMenu[]>('/sys_menu/list', data)
},
detail(id: string) {
return get<MenuTypes.SysMenu>('/sys_menu/detail', {id})

View File

@ -95,6 +95,7 @@ import {
TableColumnSelectable
} from '@idux/components/table'
import { TablePagination } from '@idux/components/table/src/types'
import Iconfont from '@/components/iconfont/Iconfont.vue'
interface IconData {
name: string
@ -120,9 +121,9 @@ const columns: TableColumn<IconData>[] = [
{
title: '图标',
dataKey: 'name',
/* customCell: ({record}: { record: IconData }) => {
return h(Iconfont, {name: record.name})
} */
customCell: ({record}: { record: IconData }) => {
return h(Iconfont, {name: record.name as IconfontTypes.name})
}
},
{
title: '名称',

View File

@ -27,7 +27,7 @@ declare global {
children?: SysMenu[]
}
type SearchForm = Partial<Pick<SysMenu, 'title' | 'routeName'>>
type SearchForm = Partial<Pick<SysMenu, 'title' | 'routeName' | 'pid'>>
type MenuForm = Pick<SysMenu, 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName' | 'sn' | 'icon'>
type AddForm = Pick<SysMenu, 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName' | 'sn'>
type ModifyForm = Pick<SysMenu, 'id' | 'pid' | 'menuCategory' | 'title' | 'sort' | 'routeName'>

View File

@ -0,0 +1,391 @@
<script lang="ts" setup>
import times, { FMT } from '@/common/utils/times.ts'
import { ref } from 'vue'
import {
TableColumn,
TableColumnSelectable
} from '@idux/components/table'
import { TablePagination } from '@idux/components/table/src/types'
import TspApi from '@/pages/tsp/tsp-api.ts'
import { useFormGroup, } from '@idux/cdk'
import TspPhotoApi from '@/pages/tsp-photo/tsp-photo-api.ts'
const weekday = [ '周一', '周二', '周三', '周四', '周五', '周六', '周日' ]
interface DayType {
year: number
month: number
day: number
}
const visible = ref(false)
const selectedRowKeys = ref<string[]>([])
const tspPhotoForm = useFormGroup<TspPhotoTypes.TspPhotoSearchParam>({
tspId: [ undefined ],
pointName: [ undefined ],
month: [ new Date() ],
day: [ 0 ],
uploadDate: [ times.format(new Date(), FMT.date) ],
})
const days = ref<Array<Array<DayType | null>>>([])
function renderCalendar(value: Date = new Date()) {
const beginOfMonth = times.beginOfMonth(times.parse(value))
const endOfMonth = times.endOfMonth(times.parse(value))
const firstDayIndex = beginOfMonth.weekday - 1
const count = (7 - (endOfMonth.day + firstDayIndex) % 7) + (endOfMonth.day + firstDayIndex)
const days_: Array<Array<DayType | null>> = []
let temp: Array<DayType | null> = []
for (let i = 0; i < count; i++) {
if (i < firstDayIndex) {
temp.push(null)
} else if (i >= firstDayIndex && i < firstDayIndex + endOfMonth.day) {
temp.push({
year: beginOfMonth.year,
month: beginOfMonth.month,
day: i - firstDayIndex + 1
})
} else {
temp.push(null)
}
if (temp.length === 7) {
days_.push(temp)
temp = []
}
}
days.value = days_
}
function disabledDate(date: Date) {
return times.parse(date).month > times.now().month
}
const datasource = ref<TspTypes.TspData[]>()
const selectableColumn = reactive<TableColumnSelectable<TspTypes.TspData>>({
type: 'selectable',
align: 'center',
multiple: false,
showIndex: false,
trigger: 'click',
onChange: (selectedKeys) => {
if (selectedKeys.length === 0) {
const value = tspPhotoForm.get('tspId')?.getValue()
if (value != null) {
selectedRowKeys.value = [ value ]
} else {
tspPhotoForm.get('tspId')?.setValue('')
tspPhotoForm.get('pointName')?.setValue('')
}
return
}
const data = datasource.value?.find(item => item.id === (selectedKeys[0] as string))
tspPhotoForm.get('tspId')?.setValue(selectedKeys[0] as string)
tspPhotoForm.get('pointName')?.setValue(data?.pointName)
},
})
const tableSpin = ref(false)
const pagination = reactive<TablePagination>({
pageIndex: 1,
pageSize: 10,
total: 0,
size: 'sm',
showTotal: true,
onChange(pageIndex: number, pageSize: number) {
pagination.pageIndex = pageIndex
pagination.pageSize = pageSize
searchHandler()
}
})
function searchHandler() {
tableSpin.value = true
TspApi.paging({
current: pagination.pageIndex ?? 1,
size: pagination.pageSize ?? 10,
})
.then(res => {
pagination.pageIndex = res.data.current
pagination.pageSize = res.data.size
pagination.total = res.data.total
datasource.value = res.data.records
})
.finally(() => {
tableSpin.value = false
})
}
const photoUploadStatus = ref<TspPhotoTypes.ObtainStatusResult>({})
const columns: TableColumn<TspTypes.TspData>[] = [
selectableColumn,
{
title: '名称',
dataKey: 'pointName',
customCell: 'pointName'
},
{
title: '所属街道',
dataKey: 'streetName',
},
{
title: '所属小区',
dataKey: 'microdistrict',
},
{
title: '所属物业',
dataKey: 'propertyManagement'
},
]
watch(visible, (newVal) => {
if (newVal) {
searchHandler()
}
})
const computedTitle = () => {
const uploadDate = tspPhotoForm.get('uploadDate')?.getValue()
const pointName = tspPhotoForm.get('pointName')?.getValue()
return uploadDate == null || pointName == null ? '图片列表' : pointName + ' ' + times.format(times.parse(uploadDate, FMT.date), FMT.date_zh) + '上传的图片'
}
const photoListTitle = ref<string>('图片列表')
const photoList = ref<string[]>()
tspPhotoForm.get('tspId')?.watchValue(() => {
TspPhotoApi.obtainStatus(tspPhotoForm.getValue()).then(res => {
photoUploadStatus.value = res.data
})
})
tspPhotoForm.get('month')?.watchValue((month) => {
tspPhotoForm.get('day')?.setValue(0)
if (month == null) {
return
}
tspPhotoForm.get('uploadDate')?.setValue(times.format(times.parse(month), FMT.date))
TspPhotoApi.obtainStatus(tspPhotoForm.getValue()).then(res => {
photoUploadStatus.value = res.data
})
})
function onClickDate(day: DayType) {
tspPhotoForm.get('day')?.setValue(day.day)
tspPhotoForm.get('uploadDate')?.setValue(`${day.year}-${day.month < 10 ? '0' + day.month : day.month}-${day.day < 10 ? '0' + day.day : day.day}`)
photoListTitle.value = computedTitle()
TspPhotoApi.listPhoto(tspPhotoForm.getValue())
.then(res => {
photoList.value = res.data?.photos ?? []
})
}
const files = ref([])
onMounted(() => {
renderCalendar()
})
</script>
<template>
<div class="tsp-photo-wrapper">
<div class="calendar-wrapper">
<div class="calendar-title">
<IxForm :control="tspPhotoForm" class="calendar-form" layout="inline">
<IxFormItem>
<IxPopover v-model:visible="visible" closable header="小区列表" placement="bottom" trigger="click">
<IxInput control="pointName" placeholder="请选择小区"/>
<template #content>
<IxTable v-model:selectedRowKeys="selectedRowKeys" :columns="columns" :dataSource="datasource" :pagination="pagination" :spin="tableSpin" get-key="id"/>
</template>
</IxPopover>
</IxFormItem>
<IxFormItem>
<IxDatePicker :disabled-date="disabledDate" control="month" type="month" @change="renderCalendar"/>
</IxFormItem>
</IxForm>
</div>
<table class="calendar">
<thead>
<tr>
<th v-for="item in weekday" :key="'weekday-' + item" class="weekday">{{ item }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in days" :key="'day-' + index">
<td v-for="(it, i) in item" :key="'day-' + i" :class="{blank:it == null}" class="day">
<div v-if="it != null" :class="{'active-day': it.day === tspPhotoForm.get('day')?.getValue()}" class="day-content" @click="onClickDate(it)">
<div class="day-number">{{ it.day }}</div>
<IxTagGroup v-if="photoUploadStatus[it.day+''] != null && photoUploadStatus[it.day+''] > 0" :gap="0" class="day-status">
<IxTag status="success">已上传</IxTag>
<IxTag status="success">{{ photoUploadStatus[it.day + ''] }}</IxTag>
</IxTagGroup>
<IxTag v-else-if="photoUploadStatus[it.day+''] != null && photoUploadStatus[it.day+''] == 0" class="day-status" status="error">未上传</IxTag>
<div v-else class="day-status" status="error"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="photo-wrapper">
<div class="photo-title">
{{ photoListTitle }}
</div>
<div class="photo">
<IxImage v-for="(it,i) in photoList" :key="'photo--'+i" :src="it"/>
</div>
<div v-if="tspPhotoForm.get('day')?.getValue() !== 0" class="photo-bottom">
<IxUpload v-model:files="files" action="https://run.mocky.io/v3/7564bc4f-780e-43f7-bc58-467959ae3354" dragable>
<div class="drag-panel">
<IxIcon class="drag-panel-icon" name="upload"></IxIcon>
<p>拖拽上传</p>
</div>
<template #list>
<IxUploadFiles type="text"/>
</template>
</IxUpload>
<IxButton>提交</IxButton>
</div>
</div>
</div>
</template>
<style lang="stylus" scoped>
.tsp-photo-wrapper {
height: 100%
width: 100%
display flex
.calendar-wrapper {
flex 1
padding 1rem
box-sizing border-box
border-right: 1px solid rgb(211, 215, 222);
.calendar-form {
width 100%
height 4rem
:deep(.ix-form-item):first-child {
width 30%
}
:deep(.ix-form-item):nth-child(2) {
width 10%
}
}
.calendar {
width 100%
height calc(100% - 4rem);
margin-top 1rem
box-sizing border-box
table-layout fixed
border-collapse: collapse;
border: 1px solid rgb(211, 215, 222);
thead {
height 4rem
}
tbody {
height calc(100% - 4rem);
}
.day {
border-top: 1px solid rgb(211, 215, 222);
&:not(.blank) {
border: 1px solid rgb(211, 215, 222);
}
.day-content {
cursor pointer
height 100%
width 100%
display flex
flex-direction column
justify-content space-around
padding 1rem
&:hover {
background-color: rgba(59, 130, 246, 0.1);
}
&.active-day {
background-color: rgba(59, 130, 246, 0.1);
}
.day-number {
font-size 3rem
font-weight bold
}
.day-status {
width 100%;
height 20px;
:deep(.ix-space-item) {
flex 1
& > span {
width 100%
}
}
}
}
}
}
}
.photo-wrapper {
width 30%
height 100%
display flex
flex-direction column
.photo-title {
font-size 2rem
font-weight bold
padding 1rem
box-sizing border-box
border-bottom 1px solid rgb(211, 215, 222);
text-wrap: auto;
word-break: break-word;
overflow-wrap: normal;
overflow: hidden;
line-height: 1.6;
}
.photo {
flex 1
width 100%;
padding 1rem
overflow-y auto
display grid
grid-template-columns repeat(2, 1fr)
grid-auto-rows: 8rem
gap 1rem
align-items stretch
& > div {
height: 8rem;
box-sizing: border-box;
}
}
}
}
</style>

View File

@ -0,0 +1,4 @@
export default {
title: '临时收纳点照片',
component: () => import('@/pages/tsp-photo/TspPhoto.vue'),
} as RouterTypes.PageConfig

View File

@ -0,0 +1,14 @@
import { get, } from '@/common/utils/http-util.ts'
export default {
save(data?: TspPhotoTypes.TspPhotoSaveParam) {
return get<null>('/tsp_photo/save', data)
},
listPhoto(data?: TspPhotoTypes.TspPhotoSearchParam) {
return get<TspPhotoTypes.TspPhotoSearchResult>('/tsp_photo/list_photo', data)
},
obtainStatus(data?: TspPhotoTypes.TspPhotoSearchParam) {
return get<TspPhotoTypes.ObtainStatusResult>('/tsp_photo/obtain_status', data)
},
}

View File

@ -0,0 +1,26 @@
export {}
declare global {
namespace TspPhotoTypes {
interface TspPhotoSearchParam {
tspId?: string
pointName?: string
month?: Date
day?: number
uploadDate?: string
}
interface TspPhotoSearchResult {
photos?: string[]
}
interface ObtainStatusResult {
[key: string]: number | undefined | null
}
interface TspPhotoSaveParam {
tspId?: string
uploadDate?: string
photos?: string[]
}
}
}

View File

@ -21,8 +21,8 @@ declare global {
}
interface SearchParam {
pointName: string
status: 'ZhengChang' | 'FenZhengChang' | ''
pointName?: string
status?: 'ZhengChang' | 'FenZhengChang' | ''
}
interface StatisticsResult {