| | |
| | | pid: null, |
| | | }, |
| | | { |
| | | name: 'operationsMessage', |
| | | path: '/operations/message', |
| | | title: '消息通知管理', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:message', |
| | | menuType: 'page', |
| | | componentPath: '/operations/message/index.vue', |
| | | id: 900, |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'operationsPersonnel', |
| | | path: '/operations/personnel', |
| | | title: '运维人员管理', |
| | |
| | | pid: 902, |
| | | hide: true, |
| | | activeMenu: '/operations/attendance/plan', |
| | | },{ |
| | | name: 'operationsAttendancePlanCopy', |
| | | path: '/operations/attendance/plan/copy', |
| | | title: '复制排班', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'page', |
| | | componentPath: '/operations/attendance/plan/copy.vue', |
| | | id: 9026, |
| | | pid: 902, |
| | | hide: true, |
| | | activeMenu: '/operations/attendance/plan', |
| | | }, |
| | | { |
| | | name: 'service', |
| | | path: '/service', |
| | | title: '服务中心', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:list-view', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 10, |
| | | pid: null, |
| | | }, |
| | | { |
| | | name: 'serviceReview', |
| | | path: '/service/review', |
| | | title: '综合审核', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:check-one', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 1001, |
| | | pid: 10, |
| | | }, |
| | | { |
| | | name: 'serviceReviewPersonnel', |
| | | path: '/service/review/personnel', |
| | | title: '人员综合审核', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:people', |
| | | menuType: 'page', |
| | | componentPath: '/service/review/personnel/index.vue', |
| | | id: 10011, |
| | | pid: 1001, |
| | | }, |
| | | { |
| | | name: 'userContent', |
| | | path: '/user-content', |
| | | title: '用户&内容', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:user', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 11, |
| | | pid: null, |
| | | }, |
| | | { |
| | | name: 'contentCenter', |
| | | path: '/user-content/content-center', |
| | | title: '内容中心', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:file-text', |
| | | menuType: 'dir', |
| | | componentPath: null, |
| | | id: 1101, |
| | | pid: 11, |
| | | }, |
| | | { |
| | | name: 'contentCenterIndex', |
| | | path: '/user-content/content-center/index', |
| | | title: '内容中心', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:file-text', |
| | | menuType: 'page', |
| | | componentPath: '/user-content/content-center/index.vue', |
| | | id: 11011, |
| | | pid: 1101, |
| | | }, |
| | | ] |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="relative h-full"> |
| | | <div class=" h-full"> |
| | | <n-card class="w-full" style="border-radius: 6px"> |
| | | <div class="mb-6 flex items-center"> |
| | | <div class="mr-2 bg-blue-500" style="width: 3px; height: 14px"></div> |
| | |
| | | </div> |
| | | </n-card> |
| | | |
| | | <div class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4"> |
| | | <div class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-16px py-24px"> |
| | | <n-button style="width: 84px; height: 32px">取消</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px">保存</n-button> |
| | | </div> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | interface ShiftOption { |
| | | name: string |
| | | checked: boolean |
| | | dateRange: number[] | null |
| | | } |
| | | |
| | | interface FormModel { |
| | | team: string | null |
| | | shifts: ShiftOption[] |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [value: boolean] |
| | | confirm: [data: FormModel] |
| | | }>() |
| | | |
| | | const show = computed({ |
| | | get: () => props.show, |
| | | set: (value) => emit('update:show', value) |
| | | }) |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const formModel = ref<FormModel>({ |
| | | team: null, |
| | | shifts: [ |
| | | { name: '早', checked: false, dateRange: null }, |
| | | { name: '中', checked: false, dateRange: null }, |
| | | { name: '晚', checked: false, dateRange: null }, |
| | | { name: '全天', checked: false, dateRange: null } |
| | | ] |
| | | }) |
| | | |
| | | // 生成1-31的日期选项 |
| | | const dateOptions = Array.from({ length: 31 }, (_, i) => ({ |
| | | label: `${i + 1}`, |
| | | value: i + 1 |
| | | })) |
| | | |
| | | const rules: FormRules = { |
| | | team: [ |
| | | { required: true, message: '请选择队伍', trigger: 'change' } |
| | | ] |
| | | } |
| | | |
| | | // 模拟队伍数据 |
| | | const teamOptions = [ |
| | | { label: '广州白云一队', value: '广州白云一队' }, |
| | | { label: '广州白云二队', value: '广州白云二队' }, |
| | | { label: '广州南沙三队', value: '广州南沙三队' } |
| | | ] |
| | | |
| | | const handleCancel = () => { |
| | | show.value = false |
| | | resetForm() |
| | | } |
| | | |
| | | const handleConfirm = async () => { |
| | | try { |
| | | await formRef.value?.validate() |
| | | emit('confirm', formModel.value) |
| | | show.value = false |
| | | resetForm() |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | formModel.value = { |
| | | team: null, |
| | | shifts: [ |
| | | { name: '早', checked: false, dateRange: null }, |
| | | { name: '中', checked: false, dateRange: null }, |
| | | { name: '晚', checked: false, dateRange: null }, |
| | | { name: '全天', checked: false, dateRange: null } |
| | | ] |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="show" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | title="批量设置" |
| | | class="w-500px" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }"> |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="top" |
| | | require-mark-placement="left" |
| | | show-require-mark |
| | | > |
| | | <n-form-item label="队伍" path="team"> |
| | | <n-select |
| | | v-model:value="formModel.team" |
| | | :options="teamOptions" |
| | | placeholder="选择" |
| | | /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="班次" path="shifts"> |
| | | <div class="w-full flex flex-col gap-3"> |
| | | <div |
| | | v-for="shift in formModel.shifts" |
| | | :key="shift.name" |
| | | class="flex items-center gap-3" |
| | | > |
| | | <n-checkbox v-model:checked="shift.checked" style="width: 60px;"> |
| | | {{ shift.name }} |
| | | </n-checkbox> |
| | | <span class="text-gray-600">时间:</span> |
| | | <n-select |
| | | v-model:value="shift.dateRange" |
| | | :disabled="!shift.checked" |
| | | :options="dateOptions" |
| | | placeholder="选择" |
| | | multiple |
| | | style="flex: 1" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="handleCancel">取消</n-button> |
| | | <n-button type="primary" @click="handleConfirm">保存</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | const router = useRouter() |
| | | |
| | | interface FormModel { |
| | | month: number | null |
| | | role: string | null |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [value: boolean] |
| | | confirm: [data: FormModel] |
| | | }>() |
| | | |
| | | const show = computed({ |
| | | get: () => props.show, |
| | | set: (value) => emit('update:show', value) |
| | | }) |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const formModel = ref<FormModel>({ |
| | | month: null, |
| | | role: null |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | month: [ |
| | | { required: true, type: 'number', message: '请选择排班月份', trigger: 'change' } |
| | | ], |
| | | role: [ |
| | | { required: true, message: '请选择角色', trigger: 'change' } |
| | | ] |
| | | } |
| | | |
| | | // 模拟角色数据 |
| | | const roleOptions = [ |
| | | { label: '网格员', value: '网格员' }, |
| | | { label: '管理员', value: '管理员' }, |
| | | { label: '巡查员', value: '巡查员' } |
| | | ] |
| | | |
| | | const handleCancel = () => { |
| | | show.value = false |
| | | resetForm() |
| | | } |
| | | |
| | | const handleConfirm = async () => { |
| | | try { |
| | | await formRef.value?.validate() |
| | | if (formModel.value.month && formModel.value.role) { |
| | | emit('confirm', formModel.value) |
| | | show.value = false |
| | | // 跳转到复制页面,携带月份和角色参数 |
| | | router.push({ |
| | | path: '/operations/attendance/plan/copy', |
| | | query: { |
| | | month: String(formModel.value.month), |
| | | role: formModel.value.role |
| | | } |
| | | }) |
| | | resetForm() |
| | | } |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | formModel.value = { |
| | | month: null, |
| | | role: null |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="show" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | title="复制排班" |
| | | class="w-500px" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }"> |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="top" |
| | | require-mark-placement="left" |
| | | show-require-mark |
| | | > |
| | | <n-form-item label="排班月份" path="month"> |
| | | <n-date-picker |
| | | v-model:value="formModel.month" |
| | | type="month" |
| | | placeholder="请选择月份" |
| | | style="width: 100%" |
| | | format="yyyy-MM" |
| | | /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="角色" path="role"> |
| | | <n-select |
| | | v-model:value="formModel.role" |
| | | :options="roleOptions" |
| | | placeholder="请选择角色" |
| | | /> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="handleCancel">取消</n-button> |
| | | <n-button type="primary" @click="handleConfirm">排班</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| | |
| | | if (formModel.value.month) { |
| | | emit('confirm', formModel.value.month) |
| | | show.value = false |
| | | resetForm() |
| | | // 跳转到编辑页面,携带月份参数 |
| | | router.push({ |
| | | path: '/operations/attendance/plan/edit', |
| | | query: { month: formModel.value.month } |
| | | query: { month: String(formModel.value.month) } |
| | | }) |
| | | resetForm() |
| | | } |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, onMounted, onUnmounted, nextTick } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | |
| | | interface ShiftTag { |
| | | name: string |
| | | time: string |
| | | color: string |
| | | bgColor: string |
| | | } |
| | | |
| | | interface TeamSchedule { |
| | | team: string |
| | | schedules: ShiftTag[][] |
| | | } |
| | | |
| | | const route = useRoute() |
| | | |
| | | const currentDate = ref('2024/12/01') |
| | | const selectedMonth = ref<number>(new Date(2024, 11, 1).valueOf()) |
| | | const selectedRole = ref<string>('') |
| | | |
| | | const shiftTags = ref<ShiftTag[]>([ |
| | | { name: '全班', time: '08:00-16:00', color: '#0068FF', bgColor: '#E6F4FF' }, |
| | | { name: '早班', time: '08:00-20:00', color: '#16A60C', bgColor: '#DCF5DD' }, |
| | | { name: '中班', time: '16:00-24:00', color: '#FF9000', bgColor: '#FFF8E6' }, |
| | | { name: '晚班', time: '19:00-24:00', color: '#764DFF', bgColor: '#F6F0FF' } |
| | | ]) |
| | | |
| | | // 生成日期列表 |
| | | const generateDates = (year: number, month: number) => { |
| | | const dates = [] |
| | | const daysInMonth = new Date(year, month + 1, 0).getDate() |
| | | for (let i = 1; i <= daysInMonth; i++) { |
| | | const date = new Date(year, month, i) |
| | | const weekDay = ['日', '一', '二', '三', '四', '五', '六'][date.getDay()] |
| | | dates.push({ |
| | | month: String(month + 1), |
| | | day: i, |
| | | weekDay: weekDay |
| | | }) |
| | | } |
| | | return dates |
| | | } |
| | | |
| | | const dates = ref(generateDates(2024, 11)) |
| | | |
| | | const teamSchedules = ref<TeamSchedule[]>([ |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云二队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州南沙三队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }, |
| | | { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) } |
| | | ]) |
| | | |
| | | // 初始化一些示例数据 |
| | | teamSchedules.value[0].schedules[3] = [shiftTags.value[0]] |
| | | teamSchedules.value[1].schedules[1] = [shiftTags.value[1]] |
| | | |
| | | const selectedCell = ref<{ teamIndex: number; dateIndex: number } | null>(null) |
| | | const selectedShifts = ref<ShiftTag[]>([]) |
| | | const selectorPosition = ref({ top: 0, left: 0 }) |
| | | |
| | | const updateDates = () => { |
| | | const date = new Date(selectedMonth.value) |
| | | const year = date.getFullYear() |
| | | const month = date.getMonth() |
| | | currentDate.value = `${year}/${String(month + 1).padStart(2, '0')}/01` |
| | | dates.value = generateDates(year, month) |
| | | |
| | | // 重新初始化排班数据 |
| | | const daysInMonth = new Date(year, month + 1, 0).getDate() |
| | | teamSchedules.value.forEach(schedule => { |
| | | schedule.schedules = Array(daysInMonth).fill(null).map(() => []) |
| | | }) |
| | | } |
| | | |
| | | const getDisplayDate = () => { |
| | | const date = new Date(selectedMonth.value) |
| | | const year = date.getFullYear() |
| | | const month = date.getMonth() + 1 |
| | | return `${year}年${month}月` |
| | | } |
| | | |
| | | const handleSave = () => { |
| | | console.log('保存') |
| | | } |
| | | |
| | | const getShiftCount = (schedules: ShiftTag[][], shiftName: string) => { |
| | | let count = 0 |
| | | schedules.forEach(shifts => { |
| | | count += shifts.filter(s => s.name === shiftName).length |
| | | }) |
| | | return count |
| | | } |
| | | |
| | | const showScheduleModal = ref(false) |
| | | |
| | | const handleCellClick = (teamIndex: number, dateIndex: number, event: MouseEvent) => { |
| | | event.stopPropagation() // 阻止事件冒泡 |
| | | |
| | | selectedCell.value = { teamIndex, dateIndex } |
| | | const currentShifts = teamSchedules.value[teamIndex].schedules[dateIndex] |
| | | selectedShifts.value = currentShifts ? [...currentShifts] : [] |
| | | |
| | | // 计算盒子位置 |
| | | const target = event.currentTarget as HTMLElement |
| | | const rect = target.getBoundingClientRect() |
| | | const boxWidth = 240 |
| | | const windowWidth = window.innerWidth |
| | | |
| | | // 判断右侧是否有足够空间,如果没有则显示在左侧 |
| | | let left = rect.right + 2 |
| | | if (left + boxWidth > windowWidth) { |
| | | left = rect.left - boxWidth - 10 |
| | | } |
| | | |
| | | selectorPosition.value = { |
| | | top: rect.top, |
| | | left: Math.max(10, left) // 确保不会超出左边界 |
| | | } |
| | | |
| | | nextTick(() => { |
| | | showScheduleModal.value = true |
| | | }) |
| | | } |
| | | |
| | | const handleShiftToggle = (shift: ShiftTag) => { |
| | | // 单选模式:直接设置为选中的班次 |
| | | selectedShifts.value = [shift] |
| | | |
| | | // 立即更新到表格 |
| | | if (selectedCell.value) { |
| | | teamSchedules.value[selectedCell.value.teamIndex].schedules[selectedCell.value.dateIndex] = [shift] |
| | | } |
| | | |
| | | // 选择后直接关闭 |
| | | closeScheduleModal() |
| | | } |
| | | |
| | | // 点击外部关闭选择盒子 |
| | | const handleClickOutside = (event: MouseEvent) => { |
| | | if (!showScheduleModal.value) return |
| | | |
| | | const target = event.target as HTMLElement |
| | | const selectorBox = document.querySelector('.schedule-selector-box') |
| | | |
| | | if (selectorBox && !selectorBox.contains(target)) { |
| | | closeScheduleModal() |
| | | } |
| | | } |
| | | |
| | | const closeScheduleModal = () => { |
| | | showScheduleModal.value = false |
| | | selectedCell.value = null |
| | | } |
| | | |
| | | onMounted(() => { |
| | | document.addEventListener('click', handleClickOutside) |
| | | |
| | | // 从路由参数读取月份 |
| | | const monthParam = route.query.month |
| | | if (monthParam) { |
| | | const monthValue = typeof monthParam === 'string' ? Number(monthParam) : Array.isArray(monthParam) ? Number(monthParam[0]) : monthParam |
| | | if (monthValue && !isNaN(monthValue)) { |
| | | selectedMonth.value = monthValue |
| | | updateDates() |
| | | } |
| | | } |
| | | |
| | | // 从路由参数读取角色 |
| | | const roleParam = route.query.role |
| | | if (roleParam) { |
| | | selectedRole.value = typeof roleParam === 'string' ? roleParam : Array.isArray(roleParam) ? (roleParam[0] || '') : '' |
| | | } |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | document.removeEventListener('click', handleClickOutside) |
| | | }) |
| | | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;" |
| | | content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"> |
| | | <div class="flex items-center justify-between" style="margin-bottom: 12px;"> |
| | | <div class="flex items-center gap-15"> |
| | | <div class="text-lg font-bold">角色:{{ selectedRole }}</div> |
| | | <div class="text-lg font-bold">日期:{{ getDisplayDate() }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="flex items-center justify-between"> |
| | | <div class="flex items-center"> |
| | | <div v-for="tag in shiftTags" :key="`${tag.name}-${tag.time}`" style="margin-right: 8px;" |
| | | class="flex items-center gap-1"> |
| | | <span class="rounded text-xs" :style="{ |
| | | backgroundColor: tag.bgColor, |
| | | color: tag.color, |
| | | width: '113px', |
| | | height: '28px', |
| | | borderRadius: '4px', |
| | | display: 'inline-flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center' |
| | | }"> |
| | | {{ tag.name }} {{ tag.time }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button> |
| | | </div> |
| | | </div> |
| | | <div class="schedule-table-container" style="overflow-x: auto; flex: 1;margin-top: 12px;"> |
| | | <table class="schedule-table"> |
| | | <thead> |
| | | <tr> |
| | | <th class="sticky-col">车队</th> |
| | | <th v-for="(date, index) in dates" :key="index"> |
| | | <div>{{ date.month }}/{{ date.day }}</div> |
| | | <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div> |
| | | </th> |
| | | <th class="summary-col">排班小计</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex"> |
| | | <td class="sticky-col team-name">{{ schedule.team }}</td> |
| | | <td v-for="(shifts, dateIndex) in schedule.schedules" :key="dateIndex" class="schedule-cell" |
| | | :class="{ 'schedule-cell-selected': selectedCell && selectedCell.teamIndex === teamIndex && selectedCell.dateIndex === dateIndex }" |
| | | @click="handleCellClick(teamIndex, dateIndex, $event)"> |
| | | <div class="flex flex-wrap gap-1"> |
| | | <span v-for="(shift, shiftIndex) in shifts" :key="shiftIndex" class="whitespace-nowrap" |
| | | :style="{ backgroundColor: shift.bgColor, color: shift.color }"> |
| | | {{ shift.name }} |
| | | </span> |
| | | </div> |
| | | </td> |
| | | <td class="summary-cell"> |
| | | <div class="flex flex-wrap justify-center" style="gap: 2px;"> |
| | | <span v-for="tag in shiftTags" :key="`${tag.name}-${tag.time}`" class="summary-tag" |
| | | :style="{ backgroundColor: tag.bgColor, color: tag.color }"> |
| | | {{ tag.name }} {{ getShiftCount(schedule.schedules, tag.name) }} |
| | | </span> |
| | | </div> |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <!-- 排班选择盒子 --> |
| | | <div v-if="showScheduleModal" class="schedule-selector-box" |
| | | :style="{ top: `${selectorPosition.top}px`, left: `${selectorPosition.left}px` }"> |
| | | <div class="schedule-selector-header"> |
| | | <span class="schedule-selector-title">排班选择</span> |
| | | <n-icon size="20" class="cursor-pointer" @click="closeScheduleModal"> |
| | | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" |
| | | stroke-linecap="round" stroke-linejoin="round"> |
| | | <line x1="18" y1="6" x2="6" y2="18"></line> |
| | | <line x1="6" y1="6" x2="18" y2="18"></line> |
| | | </svg> |
| | | </n-icon> |
| | | </div> |
| | | <div class="schedule-selector-content"> |
| | | <div v-for="shift in shiftTags" :key="`${shift.name}-${shift.time}`" class="shift-option" |
| | | @click="handleShiftToggle(shift)"> |
| | | <n-radio :checked="selectedShifts.some(s => s.name === shift.name && s.time === shift.time)" /> |
| | | <span class="shift-tag" :style="{ backgroundColor: shift.bgColor, color: shift.color }"> |
| | | {{ shift.name }} {{ shift.time }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .schedule-table-container { |
| | | position: relative; |
| | | } |
| | | |
| | | .schedule-table { |
| | | border-collapse: collapse; |
| | | background: white; |
| | | table-layout: fixed; |
| | | } |
| | | |
| | | .schedule-table th, |
| | | .schedule-table td { |
| | | border: 1px solid #e5e7eb; |
| | | text-align: center; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | .schedule-table th { |
| | | background: #F4F6F8; |
| | | font-weight: 500; |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 10; |
| | | min-width: 52px; |
| | | width: 52px; |
| | | height: 62px; |
| | | padding: 4px; |
| | | } |
| | | |
| | | .schedule-table th.sticky-col { |
| | | min-width: 148px; |
| | | width: 148px; |
| | | height: 62px; |
| | | } |
| | | |
| | | .sticky-col { |
| | | position: sticky; |
| | | left: 0; |
| | | background: white; |
| | | z-index: 11; |
| | | min-width: 148px; |
| | | width: 148px; |
| | | } |
| | | |
| | | .schedule-table th.sticky-col { |
| | | z-index: 12; |
| | | background: #F4F6F8; |
| | | } |
| | | |
| | | .team-name { |
| | | font-weight: 500; |
| | | text-align: center; |
| | | height: 40px; |
| | | padding: 8px; |
| | | } |
| | | |
| | | .schedule-cell { |
| | | cursor: pointer; |
| | | transition: background-color 0.2s; |
| | | min-width: 52px; |
| | | width: 52px; |
| | | height: 40px; |
| | | padding: 2px; |
| | | } |
| | | |
| | | .schedule-cell:hover { |
| | | background-color: #f9fafb; |
| | | } |
| | | |
| | | .schedule-cell-selected { |
| | | background-color: #E6F4FF !important; |
| | | border: 2px solid #0068FF !important; |
| | | } |
| | | |
| | | .schedule-cell .flex { |
| | | height: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .schedule-cell span { |
| | | width: 48px; |
| | | height: 36px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .summary-col { |
| | | min-width: 260px; |
| | | width: 260px; |
| | | position: sticky; |
| | | right: 0; |
| | | background: #f9fafb; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .summary-cell { |
| | | min-width: 260px; |
| | | width: 260px; |
| | | padding: 4px; |
| | | position: sticky; |
| | | right: 0; |
| | | background: white; |
| | | z-index: 9; |
| | | } |
| | | |
| | | .summary-tag { |
| | | width: 61px; |
| | | height: 36px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .schedule-selector-box { |
| | | position: fixed; |
| | | background: white; |
| | | width: 240px; |
| | | height: 224px; |
| | | border-radius: 8px; |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); |
| | | overflow: hidden; |
| | | z-index: 1000; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .schedule-selector-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 16px; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | .schedule-selector-title { |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #333; |
| | | } |
| | | |
| | | .schedule-selector-content { |
| | | padding: 16px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | flex: 1; |
| | | } |
| | | |
| | | .shift-option { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | cursor: pointer; |
| | | padding: 0; |
| | | border-radius: 4px; |
| | | transition: background-color 0.2s; |
| | | } |
| | | |
| | | .shift-option:hover { |
| | | background-color: #f9fafb; |
| | | } |
| | | |
| | | .shift-tag { |
| | | width: 113px; |
| | | height: 28px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | } |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, onMounted, onUnmounted, nextTick } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import BatchSetModal from './components/BatchSetModal.vue' |
| | | |
| | | interface ShiftTag { |
| | | name: string |
| | |
| | | team: string |
| | | schedules: ShiftTag[][] |
| | | } |
| | | |
| | | const route = useRoute() |
| | | |
| | | const currentDate = ref('2024/12/01') |
| | | const selectedMonth = ref<number>(new Date(2024, 11, 1).valueOf()) |
| | |
| | | }) |
| | | } |
| | | |
| | | const getDisplayDate = () => { |
| | | const date = new Date(selectedMonth.value) |
| | | const year = date.getFullYear() |
| | | const month = date.getMonth() + 1 |
| | | return `${year}年${month}月` |
| | | } |
| | | |
| | | const handleBatchEdit = () => { |
| | | console.log('批量设置') |
| | | showBatchSetModal.value = true |
| | | } |
| | | |
| | | const handleSave = () => { |
| | |
| | | } |
| | | |
| | | const showScheduleModal = ref(false) |
| | | const showBatchSetModal = ref(false) |
| | | |
| | | const handleCellClick = (teamIndex: number, dateIndex: number, event: MouseEvent) => { |
| | | event.stopPropagation() // 阻止事件冒泡 |
| | |
| | | |
| | | onMounted(() => { |
| | | document.addEventListener('click', handleClickOutside) |
| | | |
| | | // 从路由参数读取月份 |
| | | const monthParam = route.query.month |
| | | if (monthParam) { |
| | | const monthValue = typeof monthParam === 'string' ? Number(monthParam) : Array.isArray(monthParam) ? Number(monthParam[0]) : monthParam |
| | | if (monthValue && !isNaN(monthValue)) { |
| | | selectedMonth.value = monthValue |
| | | updateDates() |
| | | } |
| | | } |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;" content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"> |
| | | <div class="flex items-center justify-between" style="margin-bottom: 12px;"> |
| | | <div class="text-lg font-bold">日期:2020年5月</div> |
| | | <div class="text-lg font-bold">日期:{{ getDisplayDate() }}</div> |
| | | </div> |
| | | |
| | | <div class="flex items-center justify-between"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | <BatchSetModal |
| | | v-model:show="showBatchSetModal" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import EditMonthModal from './components/EditMonthModal.vue' |
| | | import CopyScheduleModal from './components/CopyScheduleModal.vue' |
| | | |
| | | interface ShiftTag { |
| | | name: string |
| | |
| | | } |
| | | |
| | | const handleCopy = () => { |
| | | console.log('复制') |
| | | showCopyModal.value = true |
| | | } |
| | | |
| | | const handleDownload = () => { |
| | |
| | | |
| | | const showScheduleModal = ref(false) |
| | | const showEditModal = ref(false) |
| | | const showCopyModal = ref(false) |
| | | |
| | | const handleCellClick = (teamIndex: number, dateIndex: number) => { |
| | | selectedCell.value = { teamIndex, dateIndex } |
| | |
| | | :current-month="selectedMonth" |
| | | @confirm="handleConfirmEdit" |
| | | /> |
| | | |
| | | <CopyScheduleModal |
| | | v-model:show="showCopyModal" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NCheckbox } from 'naive-ui' |
| | | |
| | | interface MessageItem { |
| | | id: number |
| | | type: string |
| | | pushMethod: boolean |
| | | smsMethod: boolean |
| | | updateTime: string |
| | | } |
| | | |
| | | const tableData = ref<MessageItem[]>([ |
| | | { id: 1, type: '新任务通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 2, type: '任务状态通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 3, type: '排班变动通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 4, type: '公告通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 5, type: '新事故通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' }, |
| | | { id: 6, type: '事故状态通知', pushMethod: true, smsMethod: false, updateTime: '2025-05-07 14:53' } |
| | | ]) |
| | | |
| | | const columns: DataTableColumns<MessageItem> = [ |
| | | { |
| | | title: '序号', |
| | | key: 'id', |
| | | width: 80, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | title: '通知类型', |
| | | key: 'type', |
| | | align: 'center' |
| | | }, |
| | | { |
| | | title: '通知方式', |
| | | key: 'method', |
| | | align: 'center', |
| | | render: (row) => { |
| | | return h('div', { class: 'flex items-center justify-center gap-4' }, [ |
| | | h(NCheckbox, { |
| | | checked: row.pushMethod, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.pushMethod = value |
| | | } |
| | | }, { default: () => 'PUSH推送' }), |
| | | h(NCheckbox, { |
| | | checked: row.smsMethod, |
| | | onUpdateChecked: (value: boolean) => { |
| | | row.smsMethod = value |
| | | } |
| | | }, { default: () => '短信通知' }) |
| | | ]) |
| | | } |
| | | }, |
| | | { |
| | | title: '更新时间', |
| | | key: 'updateTime', |
| | | align: 'center' |
| | | } |
| | | ] |
| | | |
| | | const handleSave = () => { |
| | | console.log('保存', tableData.value) |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex-col"> |
| | | <n-card style="border-radius: 6px; overflow: hidden;" content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"> |
| | | <div style="flex: 1; overflow: auto;"> |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :bordered="true" |
| | | :single-line="false" |
| | | /> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <div class="flex justify-end absolute bottom-0 left-0 right-0" style="background-color: white; padding: 16px 24px; border-radius: 6px;"> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | :deep(.n-data-table-th) { |
| | | background-color: #F4F6F8; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | interface FormModel { |
| | | result: 'pass' | 'reject' |
| | | remark: string |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [value: boolean] |
| | | confirm: [data: FormModel] |
| | | }>() |
| | | |
| | | const show = computed({ |
| | | get: () => props.show, |
| | | set: (value) => emit('update:show', value) |
| | | }) |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const formModel = ref<FormModel>({ |
| | | result: 'pass', |
| | | remark: '' |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | result: [ |
| | | { required: true, message: '请选择审核结果', trigger: 'change' } |
| | | ] |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | show.value = false |
| | | resetForm() |
| | | } |
| | | |
| | | const handleConfirm = async () => { |
| | | try { |
| | | await formRef.value?.validate() |
| | | emit('confirm', formModel.value) |
| | | show.value = false |
| | | resetForm() |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | formModel.value = { |
| | | result: 'pass', |
| | | remark: '' |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | v-model:show="show" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | title="审核" |
| | | class="w-600px" |
| | | :segmented="{ |
| | | content: true, |
| | | action: true, |
| | | }"> |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="80" |
| | | require-mark-placement="left" |
| | | > |
| | | <n-form-item label="审核" path="result" required> |
| | | <n-radio-group v-model:value="formModel.result"> |
| | | <n-radio value="pass">通过</n-radio> |
| | | <n-radio value="reject">不通过</n-radio> |
| | | </n-radio-group> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="备注" path="remark"> |
| | | <n-input |
| | | v-model:value="formModel.remark" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :rows="4" |
| | | /> |
| | | </n-form-item> |
| | | </n-form> |
| | | |
| | | <template #action> |
| | | <n-space justify="end"> |
| | | <n-button @click="handleCancel">取消</n-button> |
| | | <n-button type="primary" @click="handleConfirm">保存</n-button> |
| | | </n-space> |
| | | </template> |
| | | </n-modal> |
| | | </template> |
| | | |
| | | <style scoped></style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, h } from 'vue' |
| | | import type { DataTableColumns } from 'naive-ui' |
| | | import { NButton, NTag, NAvatar, NImage } from 'naive-ui' |
| | | import ReviewModal from './components/ReviewModal.vue' |
| | | |
| | | interface PersonnelItem { |
| | | id: number |
| | | name: string |
| | | account: string |
| | | phone: string |
| | | position: string |
| | | status: string |
| | | idCardFront: string |
| | | idCardBack: string |
| | | driverLicense: string |
| | | qualificationCert: string |
| | | noCriminalRecord: string |
| | | } |
| | | |
| | | const searchForm = ref({ |
| | | name: '', |
| | | account: '', |
| | | position: null, |
| | | status: null |
| | | }) |
| | | |
| | | const positionOptions = [ |
| | | { label: '信息员', value: '信息员' }, |
| | | { label: '网格员', value: '网格员' }, |
| | | { label: '管理员', value: '管理员' } |
| | | ] |
| | | |
| | | const statusOptions = [ |
| | | { label: '待提交', value: 'pending' }, |
| | | { label: '待审核', value: 'review' }, |
| | | { label: '已通过', value: 'approved' }, |
| | | { label: '已驳回', value: 'rejected' } |
| | | ] |
| | | |
| | | const tableData = ref<PersonnelItem[]>([ |
| | | { |
| | | id: 1, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '信息员', |
| | | status: '网格员', |
| | | idCardFront: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | idCardBack: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | driverLicense: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | qualificationCert: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg', |
| | | noCriminalRecord: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg' |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '待ID1', |
| | | status: '', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | qualificationCert: '', |
| | | noCriminalRecord: '' |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '待ID1', |
| | | status: '', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | qualificationCert: '', |
| | | noCriminalRecord: '' |
| | | }, |
| | | { |
| | | id: 4, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '待ID1', |
| | | status: '', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | qualificationCert: '', |
| | | noCriminalRecord: '' |
| | | }, |
| | | { |
| | | id: 5, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '待ID1', |
| | | status: '', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | qualificationCert: '', |
| | | noCriminalRecord: '' |
| | | }, |
| | | { |
| | | id: 6, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '待ID1', |
| | | status: '', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | qualificationCert: '', |
| | | noCriminalRecord: '' |
| | | }, |
| | | { |
| | | id: 7, |
| | | name: '王美丽', |
| | | account: '3948716', |
| | | phone: '14725836905', |
| | | position: '待ID1', |
| | | status: '', |
| | | idCardFront: '', |
| | | idCardBack: '', |
| | | driverLicense: '', |
| | | qualificationCert: '', |
| | | noCriminalRecord: '' |
| | | } |
| | | ]) |
| | | |
| | | const columns: DataTableColumns<PersonnelItem> = [ |
| | | { |
| | | type: 'selection' |
| | | }, |
| | | { |
| | | title: '序号', |
| | | key: 'id', |
| | | width: 80, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | title: '姓名', |
| | | key: 'name', |
| | | width: 120, |
| | | align: 'center', |
| | | render: (row) => { |
| | | return h('div', { class: 'flex items-center justify-center gap-2' }, [ |
| | | h(NAvatar, { |
| | | round: true, |
| | | size: 32, |
| | | style: { backgroundColor: '#e0e0e0' } |
| | | }, { default: () => row.name.charAt(0) }), |
| | | h('span', row.name) |
| | | ]) |
| | | } |
| | | }, |
| | | { |
| | | title: '编号', |
| | | key: 'account', |
| | | width: 120, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | title: '联系电话', |
| | | key: 'phone', |
| | | width: 140, |
| | | align: 'center' |
| | | }, |
| | | { |
| | | title: '岗位', |
| | | key: 'position', |
| | | width: 150, |
| | | align: 'center', |
| | | render: (row) => { |
| | | if (row.position === '信息员') { |
| | | return h(NTag, { type: 'info', size: 'small' }, { default: () => row.position }) |
| | | } |
| | | return h(NTag, { type: 'default', size: 'small' }, { default: () => row.position }) |
| | | } |
| | | }, |
| | | { |
| | | title: '身份证', |
| | | key: 'idCard', |
| | | width: 150, |
| | | align: 'center', |
| | | render: (row) => { |
| | | if (row.idCardFront && row.idCardBack) { |
| | | return h('div', { class: 'flex gap-1 justify-center' }, [ |
| | | h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: row.idCardFront, |
| | | objectFit: 'cover', |
| | | style: { borderRadius: '4px' }, |
| | | previewSrc: row.idCardFront |
| | | }), |
| | | h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: row.idCardBack, |
| | | objectFit: 'cover', |
| | | style: { borderRadius: '4px' }, |
| | | previewSrc: row.idCardBack |
| | | }) |
| | | ]) |
| | | } |
| | | return h('div', { |
| | | class: 'flex gap-1 justify-center' |
| | | }, [ |
| | | h('div', { |
| | | style: { |
| | | width: '60px', |
| | | height: '40px', |
| | | backgroundColor: '#f0f0f0', |
| | | borderRadius: '4px', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center' |
| | | } |
| | | }, '📷'), |
| | | h('div', { |
| | | style: { |
| | | width: '60px', |
| | | height: '40px', |
| | | backgroundColor: '#f0f0f0', |
| | | borderRadius: '4px', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center' |
| | | } |
| | | }, '📷') |
| | | ]) |
| | | } |
| | | }, |
| | | { |
| | | title: '驾驶证', |
| | | key: 'driverLicense', |
| | | width: 100, |
| | | align: 'center', |
| | | render: (row) => { |
| | | if (row.driverLicense) { |
| | | return h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: row.driverLicense, |
| | | objectFit: 'cover', |
| | | style: { borderRadius: '4px' }, |
| | | previewSrc: row.driverLicense |
| | | }) |
| | | } |
| | | return h('div', { |
| | | style: { |
| | | width: '60px', |
| | | height: '40px', |
| | | backgroundColor: '#f0f0f0', |
| | | borderRadius: '4px', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', |
| | | margin: '0 auto' |
| | | } |
| | | }, '📷') |
| | | } |
| | | }, |
| | | { |
| | | title: '无犯罪记录证明', |
| | | key: 'noCriminalRecord', |
| | | width: 140, |
| | | align: 'center', |
| | | render: (row) => { |
| | | if (row.noCriminalRecord) { |
| | | return h(NImage, { |
| | | width: 60, |
| | | height: 40, |
| | | src: row.noCriminalRecord, |
| | | objectFit: 'cover', |
| | | style: { borderRadius: '4px' }, |
| | | previewSrc: row.noCriminalRecord |
| | | }) |
| | | } |
| | | return h('div', { |
| | | style: { |
| | | width: '60px', |
| | | height: '40px', |
| | | backgroundColor: '#f0f0f0', |
| | | borderRadius: '4px', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', |
| | | margin: '0 auto' |
| | | } |
| | | }, '📷') |
| | | } |
| | | }, |
| | | { |
| | | title: '操作', |
| | | key: 'actions', |
| | | width: 150, |
| | | align: 'center', |
| | | fixed: 'right', |
| | | render: (row) => { |
| | | return h('div', { class: 'flex items-center justify-center gap-2' }, [ |
| | | h(NButton, { |
| | | text: true, |
| | | type: 'primary', |
| | | onClick: () => handleApprove(row.id) |
| | | }, { default: () => '审核' }), |
| | | h(NButton, { |
| | | text: true, |
| | | type: 'error', |
| | | onClick: () => handleDelete(row.id) |
| | | }, { default: () => '删除' }) |
| | | ]) |
| | | } |
| | | } |
| | | ] |
| | | |
| | | const pagination = ref({ |
| | | page: 1, |
| | | pageSize: 10, |
| | | itemCount: 800, |
| | | showSizePicker: true, |
| | | pageSizes: [10, 20, 50, 100] |
| | | }) |
| | | |
| | | const showReviewModal = ref(false) |
| | | const selectedIds = ref<number[]>([]) |
| | | |
| | | const handleSearch = () => { |
| | | console.log('搜索', searchForm.value) |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | searchForm.value = { |
| | | name: '', |
| | | account: '', |
| | | position: null, |
| | | status: null |
| | | } |
| | | } |
| | | |
| | | const handleBatchReview = () => { |
| | | if (selectedIds.value.length === 0) { |
| | | window.$message?.warning('请先选择要审核的人员') |
| | | return |
| | | } |
| | | showReviewModal.value = true |
| | | } |
| | | |
| | | const handleApprove = (id: number) => { |
| | | selectedIds.value = [id] |
| | | showReviewModal.value = true |
| | | } |
| | | |
| | | const handleDelete = (id: number) => { |
| | | window.$dialog?.warning({ |
| | | title: '确认删除', |
| | | content: '确定要删除该人员吗?', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | onPositiveClick: () => { |
| | | console.log('删除', id) |
| | | window.$message?.success('删除成功') |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const handleConfirmReview = (data: any) => { |
| | | console.log('审核结果', data, '选中的ID', selectedIds.value) |
| | | window.$message?.success('审核成功') |
| | | selectedIds.value = [] |
| | | } |
| | | |
| | | const handleCheck = (rowKeys: Array<string | number>) => { |
| | | selectedIds.value = rowKeys as number[] |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-form :model="searchForm" label-placement="left" inline :show-feedback="false"> |
| | | <div class="flex justify-between items-center w-full"> |
| | | <div class="flex gap-4"> |
| | | <n-form-item label="员工姓名"> |
| | | <n-input v-model:value="searchForm.name" placeholder="请输入" clearable style="width: 200px" /> |
| | | </n-form-item> |
| | | <n-form-item label="联系电话"> |
| | | <n-input v-model:value="searchForm.account" placeholder="请输入" clearable style="width: 200px" /> |
| | | </n-form-item> |
| | | <n-form-item label="岗位"> |
| | | <n-select |
| | | v-model:value="searchForm.position" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 200px" |
| | | :options="positionOptions" |
| | | /> |
| | | </n-form-item> |
| | | <n-form-item label="审核状态"> |
| | | <n-select |
| | | v-model:value="searchForm.status" |
| | | placeholder="请选择" |
| | | clearable |
| | | style="width: 200px" |
| | | :options="statusOptions" |
| | | /> |
| | | </n-form-item> |
| | | </div> |
| | | <div class="flex gap-2"> |
| | | <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button> |
| | | </div> |
| | | </div> |
| | | </n-form> |
| | | </n-card> |
| | | |
| | | <n-card style="border-radius: 6px; margin-top: 12px;" content-style="padding: 12px"> |
| | | <div class="mb-4 flex justify-end gap-2"> |
| | | <n-button style="width: 84px; height: 32px;color: #0068FF; background-color: #E6F4FF; border: 1px solid #7ABDFF;" @click="handleBatchReview">批量审核</n-button> |
| | | </div> |
| | | |
| | | <n-data-table |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :pagination="{ |
| | | page: pagination.page, |
| | | pageSize: pagination.pageSize, |
| | | itemCount: pagination.itemCount, |
| | | showSizePicker: true, |
| | | pageSizes: pagination.pageSizes, |
| | | prefix: ({ itemCount }) => `共 ${itemCount || 0} 条` |
| | | }" |
| | | :scroll-x="1400" |
| | | :row-key="(row: PersonnelItem) => row.id" |
| | | @update:checked-row-keys="handleCheck" |
| | | /> |
| | | </n-card> |
| | | |
| | | <ReviewModal |
| | | v-model:show="showReviewModal" |
| | | @confirm="handleConfirmReview" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | interface Step { |
| | | id: number |
| | | title: string |
| | | content: string |
| | | images: string[] |
| | | } |
| | | |
| | | interface FormModel { |
| | | images: string[] |
| | | steps: Step[] |
| | | } |
| | | |
| | | const contentType = ref('image') |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const formModel = ref<FormModel>({ |
| | | images: [], |
| | | steps: [ |
| | | { id: 1, title: '', content: '', images: [] } |
| | | ] |
| | | }) |
| | | |
| | | const rules = computed<FormRules>(() => { |
| | | if (contentType.value === 'image') { |
| | | return { |
| | | images: [ |
| | | { required: true, type: 'array', min: 1, message: '请上传图片', trigger: 'change' } |
| | | ] |
| | | } |
| | | } else { |
| | | const stepRules: FormRules = {} |
| | | formModel.value.steps.forEach((_, index) => { |
| | | stepRules[`steps[${index}].title`] = [ |
| | | { required: true, message: '请输入标题', trigger: 'blur' } |
| | | ] |
| | | stepRules[`steps[${index}].images`] = [ |
| | | { required: true, type: 'array', min: 1, message: '请上传图片', trigger: 'change' } |
| | | ] |
| | | }) |
| | | return stepRules |
| | | } |
| | | }) |
| | | |
| | | const handleUpload = (options: any) => { |
| | | console.log('上传文件', options) |
| | | } |
| | | |
| | | const addStep = () => { |
| | | const newId = formModel.value.steps.length + 1 |
| | | formModel.value.steps.push({ id: newId, title: '', content: '', images: [] }) |
| | | } |
| | | |
| | | const deleteStep = (index: number) => { |
| | | if (formModel.value.steps.length > 1) { |
| | | formModel.value.steps.splice(index, 1) |
| | | } |
| | | } |
| | | |
| | | const validate = async () => { |
| | | return await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getData: () => ({ |
| | | contentType: contentType.value, |
| | | images: formModel.value.images, |
| | | steps: formModel.value.steps |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="flex gap-4"> |
| | | <!-- 左侧内容区 --> |
| | | <div style="flex: 1; max-width: 800px;"> |
| | | <n-tabs |
| | | v-model:value="contentType" |
| | | type="segment" |
| | | size="small" |
| | | :theme-overrides="{ |
| | | tabColorSegment: '#0068FF', |
| | | tabTextColorActiveSegment: '#ffffff', |
| | | tabTextColorSegment: '#333333' |
| | | }" |
| | | class="mb-4" |
| | | style="width: 200px;" |
| | | > |
| | | <n-tab-pane name="image" tab="整图" /> |
| | | <n-tab-pane name="steps" tab="分步骤" /> |
| | | </n-tabs> |
| | | |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="80" |
| | | require-mark-placement="left" |
| | | > |
| | | <!-- 整图模式 --> |
| | | <div v-if="contentType === 'image'"> |
| | | <n-form-item label="图片" path="images" required> |
| | | <n-upload |
| | | :max="10" |
| | | list-type="image-card" |
| | | :custom-request="handleUpload" |
| | | > |
| | | <div style="width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; font-size: 32px; color: #999;"> |
| | | + |
| | | </div> |
| | | </n-upload> |
| | | </n-form-item> |
| | | </div> |
| | | |
| | | <!-- 分步骤模式 --> |
| | | <div v-else> |
| | | <n-button type="primary" @click="addStep" class="mb-4">添加步骤</n-button> |
| | | |
| | | <div v-for="(step, index) in formModel.steps" :key="step.id" class="mb-6"> |
| | | <div class="flex items-center gap-2 mb-4"> |
| | | <span>步骤{{ index + 1 }}:</span> |
| | | <n-button text type="error" @click="deleteStep(index)"> |
| | | <template #icon> |
| | | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" style="width: 18px; height: 18px; color: #ff4d4f;"> |
| | | <path d="M12 12h2v12h-2z" fill="currentColor"></path> |
| | | <path d="M18 12h2v12h-2z" fill="currentColor"></path> |
| | | <path d="M4 6v2h2v20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h2V6zm4 22V8h16v20z" fill="currentColor"></path> |
| | | <path d="M12 2h8v2h-8z" fill="currentColor"></path> |
| | | </svg> |
| | | </template> |
| | | </n-button> |
| | | </div> |
| | | |
| | | <n-form-item label="标题" :path="`steps[${index}].title`" required> |
| | | <n-input v-model:value="step.title" placeholder="请输入" style="width: 300px;" /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="内容" :path="`steps[${index}].content`"> |
| | | <n-input |
| | | v-model:value="step.content" |
| | | placeholder="请输入" |
| | | style="width: 300px;" |
| | | /> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="图片" :path="`steps[${index}].images`" required> |
| | | <n-upload |
| | | :max="10" |
| | | list-type="image-card" |
| | | :custom-request="handleUpload" |
| | | > |
| | | <div style="width: 100px; height: 100px; display: flex; align-items: center; justify-content: center; font-size: 32px; color: #999;"> |
| | | + |
| | | </div> |
| | | </n-upload> |
| | | </n-form-item> |
| | | </div> |
| | | </div> |
| | | </n-form> |
| | | </div> |
| | | |
| | | <!-- 右侧手机预览 --> |
| | | <div style="width: 280px; flex-shrink: 0;"> |
| | | <div class="phone-preview"> |
| | | <img src="@/assets/service/phone.png" alt="手机预览" style="width: 100%; height: auto;" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .phone-preview { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | |
| | | interface FaqItem { |
| | | id: number |
| | | question: string |
| | | answer: string |
| | | } |
| | | |
| | | interface FormModel { |
| | | items: FaqItem[] |
| | | } |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const formModel = ref<FormModel>({ |
| | | items: [ |
| | | { id: 1, question: '', answer: '' }, |
| | | { id: 2, question: '', answer: '' }, |
| | | { id: 3, question: '', answer: '' } |
| | | ] |
| | | }) |
| | | |
| | | const rules: FormRules = {} |
| | | |
| | | const addRow = () => { |
| | | const newId = formModel.value.items.length + 1 |
| | | formModel.value.items.push({ id: newId, question: '', answer: '' }) |
| | | } |
| | | |
| | | const deleteRow = (index: number) => { |
| | | if (formModel.value.items.length > 1) { |
| | | formModel.value.items.splice(index, 1) |
| | | } |
| | | } |
| | | |
| | | const validate = async () => { |
| | | return await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getData: () => formModel.value |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | > |
| | | <n-table :bordered="true" :single-line="false"> |
| | | <thead> |
| | | <tr> |
| | | <th style="width: 80px; text-align: center;">序号</th> |
| | | <th style="width: 250px;">常见问题</th> |
| | | <th>说明</th> |
| | | <th style="width: 100px; text-align: center;">操作</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="(item, index) in formModel.items" :key="item.id"> |
| | | <td style="text-align: center;">{{ index + 1 }}</td> |
| | | <td> |
| | | <n-input |
| | | v-model:value="item.question" |
| | | placeholder="请输入" |
| | | :bordered="false" |
| | | /> |
| | | </td> |
| | | <td> |
| | | <n-input |
| | | v-model:value="item.answer" |
| | | type="textarea" |
| | | placeholder="请输入" |
| | | :bordered="false" |
| | | :autosize="{ minRows: 3, maxRows: 6 }" |
| | | /> |
| | | </td> |
| | | <td style="text-align: center;"> |
| | | <n-button text type="primary" @click="deleteRow(index)"> |
| | | 移除 |
| | | </n-button> |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </n-table> |
| | | </n-form> |
| | | |
| | | <div class="mt-4"> |
| | | <n-button text type="primary" @click="addRow"> |
| | | + 添加行 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | :deep(.n-input__input-el) { |
| | | padding: 8px; |
| | | } |
| | | |
| | | :deep(.n-input__textarea-el) { |
| | | padding: 8px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' |
| | | |
| | | interface FormModel { |
| | | content: string |
| | | } |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const formModel = ref<FormModel>({ |
| | | content: '' |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | content: [ |
| | | { required: true, message: '请输入内容', trigger: 'blur' } |
| | | ] |
| | | } |
| | | |
| | | const validate = async () => { |
| | | return await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getData: () => formModel.value |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="80" |
| | | require-mark-placement="left" |
| | | > |
| | | <n-form-item label="" path="content" required> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.content" /> |
| | | </div> |
| | | </n-form-item> |
| | | </n-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .rich-text-wrapper { |
| | | width: 100%; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-container) { |
| | | min-height: 300px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-editor) { |
| | | min-height: 300px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import RichTextEditor from '@/components/custom/Editor/RichTextEditor/index.vue' |
| | | |
| | | interface FormModel { |
| | | tirePressure: string |
| | | lighting: string |
| | | bodyClean: string |
| | | cabinClean: string |
| | | lostItems: string |
| | | } |
| | | |
| | | const activeSubTab = ref('tirePressure') |
| | | |
| | | const subTabs = [ |
| | | { key: 'tirePressure', label: '胎压说明' }, |
| | | { key: 'lighting', label: '灯光说明' }, |
| | | { key: 'bodyClean', label: '车身标识及清洁' }, |
| | | { key: 'cabinClean', label: '舱内清洁及消毒' }, |
| | | { key: 'lostItems', label: '遗失物品上报' } |
| | | ] |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const formModel = ref<FormModel>({ |
| | | tirePressure: '', |
| | | lighting: '', |
| | | bodyClean: '', |
| | | cabinClean: '', |
| | | lostItems: '' |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | | tirePressure: [ |
| | | { required: true, message: '请输入胎压说明', trigger: 'blur' } |
| | | ], |
| | | lighting: [ |
| | | { required: true, message: '请输入灯光说明', trigger: 'blur' } |
| | | ], |
| | | bodyClean: [ |
| | | { required: true, message: '请输入车身标识及清洁说明', trigger: 'blur' } |
| | | ], |
| | | cabinClean: [ |
| | | { required: true, message: '请输入舱内清洁及消毒说明', trigger: 'blur' } |
| | | ], |
| | | lostItems: [ |
| | | { required: true, message: '请输入遗失物品上报说明', trigger: 'blur' } |
| | | ] |
| | | } |
| | | |
| | | const validate = async () => { |
| | | return await formRef.value?.validate() |
| | | } |
| | | |
| | | defineExpose({ |
| | | validate, |
| | | getData: () => formModel.value |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <n-tabs |
| | | v-model:value="activeSubTab" |
| | | type="line" |
| | | size="small" |
| | | :theme-overrides="{ |
| | | tabColorSegment: '#0068FF', |
| | | tabTextColorActiveSegment: '#ffffff', |
| | | tabTextColorSegment: '#333333' |
| | | }" |
| | | class="mb-4" |
| | | > |
| | | <n-tab-pane v-for="tab in subTabs" :key="tab.key" :name="tab.key" :tab="tab.label" /> |
| | | </n-tabs> |
| | | |
| | | <n-form |
| | | ref="formRef" |
| | | :model="formModel" |
| | | :rules="rules" |
| | | label-placement="left" |
| | | label-width="80" |
| | | require-mark-placement="left" |
| | | > |
| | | <n-form-item label="" path="tirePressure" required v-show="activeSubTab === 'tirePressure'"> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.tirePressure" /> |
| | | </div> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="" path="lighting" required v-show="activeSubTab === 'lighting'"> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.lighting" /> |
| | | </div> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="" path="bodyClean" required v-show="activeSubTab === 'bodyClean'"> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.bodyClean" /> |
| | | </div> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="" path="cabinClean" required v-show="activeSubTab === 'cabinClean'"> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.cabinClean" /> |
| | | </div> |
| | | </n-form-item> |
| | | |
| | | <n-form-item label="" path="lostItems" required v-show="activeSubTab === 'lostItems'"> |
| | | <div class="rich-text-wrapper"> |
| | | <RichTextEditor v-model="formModel.lostItems" /> |
| | | </div> |
| | | </n-form-item> |
| | | </n-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .rich-text-wrapper { |
| | | width: 100%; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-container) { |
| | | min-height: 300px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .rich-text-wrapper :deep(.ql-editor) { |
| | | min-height: 300px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import ContentForm from './components/ContentForm.vue' |
| | | import VehiclePrepareForm from './components/VehiclePrepareForm.vue' |
| | | import SimpleRichTextForm from './components/SimpleRichTextForm.vue' |
| | | import FaqForm from './components/FaqForm.vue' |
| | | |
| | | const activeTab = ref('guide') |
| | | |
| | | const tabs = [ |
| | | { key: 'guide', label: '乘车指南' }, |
| | | { key: 'prepare', label: '车辆整备' }, |
| | | { key: 'charge', label: '车辆充电' }, |
| | | { key: 'inspect', label: '车辆检测' }, |
| | | { key: 'notice', label: '乘车须知' }, |
| | | { key: 'faq', label: '常见问题' } |
| | | ] |
| | | |
| | | const contentFormRefs = ref<Record<string, any>>({}) |
| | | |
| | | const handleSave = async () => { |
| | | try { |
| | | const currentForm = contentFormRefs.value[activeTab.value] |
| | | if (currentForm) { |
| | | await currentForm.validate() |
| | | const data = currentForm.getData() |
| | | console.log('保存', activeTab.value, data) |
| | | } |
| | | } catch (error) { |
| | | console.log('验证失败', error) |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="h-full flex flex-col"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 12px"> |
| | | <n-tabs |
| | | v-model:value="activeTab" |
| | | type="segment" |
| | | :theme-overrides="{ |
| | | tabColorSegment: '#0068FF', |
| | | tabTextColorActiveSegment: '#ffffff', |
| | | tabTextColorSegment: '#333333' |
| | | }" |
| | | > |
| | | <n-tab-pane v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="tab.label"> |
| | | <VehiclePrepareForm |
| | | v-if="tab.key === 'prepare'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <SimpleRichTextForm |
| | | v-else-if="tab.key === 'charge' || tab.key === 'inspect' || tab.key === 'notice'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <FaqForm |
| | | v-else-if="tab.key === 'faq'" |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | <ContentForm |
| | | v-else |
| | | :ref="(el) => { if (el) contentFormRefs[tab.key] = el }" |
| | | /> |
| | | </n-tab-pane> |
| | | </n-tabs> |
| | | </n-card> |
| | | |
| | | <div class="flex justify-end absolute bottom-0 left-0 right-0" style="background-color: white; padding: 16px 24px; border-radius: 6px; margin-top: 12px;"> |
| | | <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | :deep(.n-tabs-nav--segment .n-tabs-tab--active) { |
| | | background-color: #0068FF !important; |
| | | } |
| | | </style> |