// 使用Vue 3 API const { createApp, ref, reactive, computed, onMounted, onShow, watch } = Vue; const { createRouter, createWebHistory } = VueRouter; const { createVuetify } = Vuetify; var toTime = function (dateStr) { var date = new Date(dateStr).toJSON(); return new Date(+new Date(date) + 8 * 3600 * 1000).toISOString().replace(/T/g, ' ').replace(/\.[\d]{3}Z/, ''); } Date.prototype.Format = function (fmt) { // 将当前 var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; // 先替换年份 if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); // 再依次替换其他时间日期内容 for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } // 日期选择组件 const DatePickerDialog = { name: 'DatePickerDialog', props: { modelValue: { type: String, default: '' }, label: { type: String, default: '选择日期' }, disabled: { type: Boolean, default: false }, format: { type: String, default: 'yyyy-MM-dd' } }, emits: ['update:modelValue'], setup(props, { emit }) { const showPicker = ref(false); const internalValue = ref(new Date()); const displayText = ref(''); // 打开选择器 const openPicker = () => { if (!props.disabled) { showPicker.value = true; } }; // 确认选择 const confirmSelection = () => { emit('update:modelValue', internalValue.value?.Format(props.format)); displayText.value = internalValue.value?.Format(props.format); showPicker.value = false; }; // 取消选择 const cancelSelection = () => { // 重置为原始值 internalValue.value = new Date(); displayText.value = internalValue.value.Format(props.format); showPicker.value = false; }; // 组件挂载时格式化显示 onMounted(() => { if (props.modelValue != '') { internalValue.value = new Date(props.modelValue); displayText.value = props.modelValue; }else{ displayText.value = internalValue.value.Format(props.format); emit('update:modelValue', displayText.value); } }); return { showPicker, internalValue, displayText, openPicker, confirmSelection, cancelSelection }; }, template: `
{{ label }} {{ displayText }} mdi-calendar {{ label }} mdi-close 取消 确认
` }; // 颜色选择按钮组件 const ColorPickerButton = { name: 'ColorPickerButton', props: { modelValue: { type: String, default: '#000000' } }, emits: ['update:modelValue'], template: `
{{ modelValue }} 选择颜色 取消 确定
`, setup(props, { emit }) { const showColorPicker = ref(false); const internalColor = ref(props.modelValue); // 根据背景色计算文字颜色(黑色或白色) const getTextColor = (bgColor) => { // 移除#号 const hex = bgColor.replace('#', ''); // 转换为RGB const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); // 计算亮度 const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 128 ? '#000000' : '#ffffff'; }; // 处理颜色变化 const handleColorChange = () => { // 实时更新模型值 emit('update:modelValue', internalColor.value); }; // 确认颜色选择 const confirmColorSelection = () => { emit('update:modelValue', internalColor.value); showColorPicker.value = false; }; return { showColorPicker, internalColor, getTextColor, handleColorChange, confirmColorSelection }; } }; // 创建Vuetify实例 const vuetify = createVuetify({ theme: { defaultTheme: 'light', themes: { light: { colors: { primary: '#1976D2', secondary: '#424242', accent: '#82B1FF', error: '#FF5252', info: '#2196F3', success: '#4CAF50', warning: '#FFC107' } } } } }); const snackbarStore = reactive({ queue: ref([]), // 成功消息 success(message, options = {}) { this.queue.push({ text: message, color: 'success', timeout: 1000 }) }, // 错误消息 error(message, options = {}) { this.queue.push({ text: message, color: 'error', timeout: 1000 }) }, // 警告消息 warning(message, options = {}) { this.queue.push({ text: message, color: 'warning', timeout: 1000 }) }, // 信息消息 info(message, options = {}) { this.queue.push({ text: message, color: 'info', timeout: 1000 }) }, // 清空队列 clear() { this.queue = []; } }); //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 全局状态管理 const store = reactive({ isLoggedIn: false, token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'), // 保存token和用户信息 saveCredentials(token, userInfo) { this.token = token; this.userInfo = userInfo; localStorage.setItem('token', token); localStorage.setItem('userInfo', JSON.stringify(userInfo)); this.isLoggedIn = true; }, // 清除token和用户信息 clearCredentials() { this.token = ''; this.userInfo = null; localStorage.removeItem('token'); localStorage.removeItem('userInfo'); this.isLoggedIn = false; }, // 检查登录状态 checkLogin() { return !!this.token; }, // 退出登录 logout() { this.clearCredentials(); router.push('/login'); }, }); //------------------------------------------------------------------------------------------------------------------------------------------------------------- const LoginComponent = { template: ` 听力健康管家 - 登录 {{ store.error }} 登录 `, setup() { const username = ref(''); const password = ref(''); const loginForm = ref(null); const loading = ref(false); const handleLogin = async () => { // 验证表单 if (!loginForm.value.validate()) { return; } loading.value = true; // 这里应该调用实际的登录API fetch('/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username.value, password: md5(password.value) }) }) .then(response => response.json()) .then(result => { loading.value = false; if (result.code === 0) { store.saveCredentials(result.data.token, { username: username.value }); router.push('/'); } else { snackbarStore.error( result.message || '登录失败,请重试'); } }) .catch(error => { console.error('登录失败:', error); loading.value = false; snackbarStore.error('网络错误,请重试'); }); }; return { store, username, password, handleLogin, loading, loginForm }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 听力师管理组件 const AudiologistsComponent = { template: ` 听力师管理 mdi-refresh刷新

确认删除

确定要删除听力师 "{{ selectedAudiologist?.nickname }}" 吗?此操作不可撤销。 取消 {{ deleting ? '删除中...' : '删除' }}

确认重置密码

确定要重置听力师 "{{ selectedAudiologist?.nickname }}" 的密码吗? 取消 {{ resetting ? '重置中...' : '重置密码' }}
`, setup() { const showDeleteDialog = ref(false); const selectedAudiologist = ref(null); const deleting = ref(false); const showResetPasswordDialog = ref(false); const resetting = ref(false); const audiologists = ref([]); const headers = ref([ { title: 'ID', key: 'id', width: '280px' }, { title: '手机号', key: 'phone' }, { title: '昵称', key: 'nickname' }, { title: '医院', key: 'hospital' }, { title: '简介', key: 'description' }, { title: '创建时间', key: 'created_at', value: (row) => toTime(row.created_at) }, { title: '操作', key: 'actions', width: '180px' } ]); const loading = ref(false); const searchParams = reactive({ keyword: '' }); const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); const loadAudiologists = async (page = 1, pageSize = 10, params = {}) => { loading.value = true; fetch('/admin/audiologists', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, body: JSON.stringify({ page, pageSize, ...params }) }) .then((response) => response.json()) .then((result) => { loading.value = false; if (result.code === 0) { audiologists.value = result.data || []; pagination.total = result.total || 0; } else { snackbarStore.error(result.message || '加载听力师数据失败'); } }).catch((error) => { loading.value = false; snackbarStore.error('加载听力师数据失败'); console.error('加载听力师列表失败:', error); }); }; const searchAudiologists = () => { pagination.page = 1; loadAudiologists(pagination.page, pagination.pageSize, searchParams); }; const resetSearch = () => { searchParams.keyword = ''; pagination.page = 1; loadAudiologists(pagination.page, pagination.pageSize, {}); }; const refreshData = () => { loadAudiologists(pagination.page, pagination.pageSize, searchParams); }; const handlePageChange = (newPage) => { pagination.page = newPage; loadAudiologists(pagination.page, pagination.pageSize, searchParams); }; const handlePageSizeChange = (newSize) => { pagination.pageSize = newSize; pagination.page = 1; loadAudiologists(pagination.page, pagination.pageSize, searchParams); }; const confirmResetPassword = (audiologist) => { selectedAudiologist.value = audiologist; showResetPasswordDialog.value = true; }; const resetPassword = async () => { loading.value = true; showResetPasswordDialog.value = false; fetch(`/admin/audiologists/${selectedAudiologist.value.id}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }).then((response) => response.json()) .then((result) => { loading.value = false; if (result.code === 0) { alert(`密码重置成功,请牢记新密码为:${result.data.password}`); } else { snackbarStore.error(result.message || '密码重置失败'); } }).catch((error) => { loading.value = false; snackbarStore.error('密码重置失败'); console.error('重置密码失败:', error); }).finally(()=>{ selectedAudiologist.value = null; }); }; // 确认删除 const confirmDelete = (audiologist) => { selectedAudiologist.value = audiologist; showDeleteDialog.value = true; }; const deleteAudiologist = async () => { deleting.value = true; showDeleteDialog.value = false; fetch(`/admin/audiologists/${selectedAudiologist.value.id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }).then((response) => response.json()) .then((result) => { deleting.value = false; if (result.code === 0) { snackbarStore.success('听力师删除成功'); loadAudiologists(pagination.page, pagination.pageSize, searchParams); } else { snackbarStore.error(result.message || '听力师删除失败'); } }).catch((error) => { deleting.value = false; snackbarStore.error('听力师删除失败'); console.error('删除听力师失败:', error); }).finally(()=>{ selectedAudiologist.value = null; }); }; // 初始加载 loadAudiologists(); return { selectedAudiologist, audiologists, headers, loading, searchParams, pagination, searchAudiologists, resetSearch, refreshData, handlePageChange, handlePageSizeChange, resetPassword, confirmDelete, deleteAudiologist, deleting, showDeleteDialog, confirmResetPassword, resetting, showResetPasswordDialog, }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 问卷分类管理组件 const QuestionnaireCategoriesComponent = { template: ` 暂无分类数据

{{ showEditDialog ? '编辑分类' : '添加分类' }}

取消 {{ saving ? '保存中...' : '保存' }}

确认删除

确定要删除分类 "{{ selectedCategory?.name }}" 吗?此操作不可撤销。 取消 {{ deleting ? '删除中...' : '删除' }}
`, setup() { const loading = ref(false); const saving = ref(false); const deleting = ref(false); const showAddDialog = ref(false); const showEditDialog = ref(false); const showDeleteDialog = ref(false); const categories = ref([]); const selectedCategory = ref(null); const newCategoryName = ref(''); // 表格列定义 const headers = [ { title: 'ID', key: 'id', width: '280px' }, { title: '分类名称', key: 'name' }, { title: '创建时间', key: 'created_at', value: (row) => toTime(row.created_at) }, { title: '操作', key: 'actions', width: '180px' } ]; // 加载分类列表 const loadCategories = async () => { loading.value = true; fetch('/admin/categories', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }) .then(response => response.json()) .then(data => { loading.value = false; if (data.code === 0) { categories.value = data.data || []; } else { snackbarStore.error(data.message || '获取分类列表失败'); } }).catch(err => { loading.value = false; snackbarStore.error('网络错误,请稍后重试'); console.error('加载分类失败:', err); }); }; // 保存分类(添加或编辑) const saveCategory = async () => { if (!newCategoryName.value?.trim()) { snackbarStore.error('请输入分类名称'); return; } saving.value = true; const isEdit = showEditDialog.value; const url = isEdit ? `/admin/categories/${selectedCategory.value.id}` : '/admin/categories/create'; fetch(url, { method: isEdit ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, body: JSON.stringify({ name: newCategoryName.value.trim() }) }) .then(response => response.json()) .then(async (data) => { saving.value = false; if (data.code === 0) { closeDialog(); await loadCategories(); snackbarStore.success(isEdit ? '分类信息更新成功' : '分类添加成功'); } else { snackbarStore.error(data.message || '操作失败,请重试'); } }).catch(err => { saving.value = false; snackbarStore.error('网络错误,请稍后重试'); console.error('保存分类失败:', err); }); }; // 删除分类 const deleteCategory = async () => { if (!selectedCategory.value) return; deleting.value = true; fetch(`/admin/categories/${selectedCategory.value.id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }) .then(response => response.json()) .then(async (data) => { deleting.value = false; if (data.code === 0) { closeDialog(); await loadCategories(); snackbarStore.success('分类删除成功'); } else { snackbarStore.error(data.message || '删除失败,请重试'); } }).catch(err => { deleting.value = false; snackbarStore.error('网络错误,请稍后重试'); console.error('删除分类失败:', err); }); }; // 编辑分类 const editCategory = (category) => { selectedCategory.value = category; newCategoryName.value = category.name; showEditDialog.value = true; }; // 确认删除 const confirmDelete = (category) => { selectedCategory.value = category; showDeleteDialog.value = true; }; // 关闭对话框 const closeDialog = () => { showAddDialog.value = false; showEditDialog.value = false; showDeleteDialog.value = false; newCategoryName.value = ''; selectedCategory.value = null; }; // 初始加载数据 loadCategories(); return { showAEDlg: computed(() => showAddDialog.value || showEditDialog.value), loading, saving, deleting, showAddDialog, showEditDialog, showDeleteDialog, categories, selectedCategory, headers, saveCategory, deleteCategory, editCategory, confirmDelete, closeDialog, loadCategories, newCategoryName }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- const DashboardComponent = { template: ` 仪表盘
{{ stat.title }}
{{ stat.value }}
{{ stat.description }}
`, setup() { const stats = reactive([ { title: '用户总数', value: 0, description: '注册用户数量' }, { title: '听力师总数', value: 0, description: '认证听力师数量' }, { title: '问卷总数', value: 0, description: '已创建问卷数量' }, { title: '答案总数', value: 0, description: '已提交答案数量' }, { title: '分享总数', value: 0, description: '报告分享记录数量' } ]); const loading = ref(false); const loadDashboardData = async () => { loading.value = true; fetch('/admin/dashboard/stats', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }) .then((response) => response.json()) .then((data) => { loading.value = false; if ( data.code === 0) { stats[0].value = data.data.total_users || 0; stats[1].value = data.data.total_audiologists || 0; stats[2].value = data.data.total_questionnaires || 0; stats[3].value = data.data.total_answers || 0; stats[4].value = data.data.total_shares || 0; } else if (data.code === 8003) { store.logout(); } else { snackbarStore.error(data.message || '加载仪表盘数据失败'); } }) .catch((error) => { loading.value = false; snackbarStore.error('网络错误,请稍后重试'); console.error('加载仪表盘数据失败:', error); }); }; // 页面加载时获取数据 loadDashboardData(); return { stats, loading, store }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- const UsersComponent = { template: ` 用户管理 mdi-refresh刷新

确认删除

确定要删除用户 "{{ selectedUser?.nickname }}" 吗?此操作不可撤销。 取消 {{ deleting ? '删除中...' : '删除' }}

确认重置密码

确定要重置用户 "{{ selectedUser?.nickname }}" 的密码吗? 取消 {{ resetting ? '重置中...' : '重置密码' }}
`, setup() { const showDeleteDialog = ref(false); const selectedUser = ref(null); const deleting = ref(false); const showResetPasswordDialog = ref(false); const resetting = ref(false); const users = ref([]); const headers = ref([ { title: 'ID', key: 'id', width: '280px' }, { title: '手机号', key: 'phone' }, { title: '昵称', key: 'nickname' }, { title: '头像', key: 'avatar' }, { title: '注册时间', key: 'created_at' }, { title: '操作', key: 'actions', sortable: false } ]); const loading = ref(false); const searchParams = reactive({ keyword: '' }); const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); const loadUsers = async (page = 1, pageSize = 10, params = {}) => { loading.value = true; try { const response = await fetch('/admin/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, body: JSON.stringify({ page, pageSize, ...params }) }); const result = await response.json(); if (result.code === 0) { users.value = result.data || []; pagination.total = result.total || 0; } else { snackbarStore.error(result.message || '加载用户数据失败'); } } catch (error) { console.error('加载用户列表失败:', error); snackbarStore.error('网络错误,请重试'); } finally { loading.value = false; } }; const searchUsers = () => { pagination.page = 1; loadUsers(pagination.page, pagination.pageSize, searchParams); }; const resetSearch = () => { searchParams.keyword = ''; pagination.page = 1; loadUsers(pagination.page, pagination.pageSize, {}); }; const refreshData = () => { loadUsers(pagination.page, pagination.pageSize, searchParams); }; const handlePageChange = (newPage) => { pagination.page = newPage; loadUsers(pagination.page, pagination.pageSize, searchParams); }; const handlePageSizeChange = (newSize) => { pagination.pageSize = newSize; pagination.page = 1; loadUsers(pagination.page, pagination.pageSize, searchParams); }; // 确认重置密码 const confirmResetPassword = (user) => { selectedUser.value = user; showResetPasswordDialog.value = true; }; const resetPassword = async () => { showResetPasswordDialog.value = false; try { loading.value = true; const response = await fetch(`/admin/users/${selectedUser.value.id}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }); const result = await response.json(); if (result.code === 0) { alert(`密码重置成功,请牢记新密码为:${result.data.password}`); } else { snackbarStore.error(result.message || '密码重置失败'); } } catch (error) { console.error('重置密码失败:', error); snackbarStore.error('网络错误,请重试'); } finally { loading.value = false; selectedUser.value = null; } }; // 确认删除 const confirmDelete = (user) => { selectedUser.value = user; showDeleteDialog.value = true; }; const deleteUser = async () => { showDeleteDialog.value = false; try { deleting.value = true; const response = await fetch(`/admin/users/${selectedUser.value.id}`, { method: 'DELETE', headers: { 'Authorization': `${store.token}` } }); const result = await response.json(); if (result.code === 0) { snackbarStore.success('用户删除成功'); loadUsers(pagination.page, pagination.pageSize, searchParams); } else { snackbarStore.error(result.message || '用户删除失败'); } } catch (error) { console.error('删除用户失败:', error); snackbarStore.error('网络错误,请重试'); } finally { deleting.value = false; selectedUser.value = null; } }; // 初始加载 loadUsers(); return { users, headers, loading, searchParams, pagination, searchUsers, resetSearch, refreshData, handlePageChange, handlePageSizeChange, resetPassword, confirmDelete, deleteUser, deleting, showDeleteDialog, confirmResetPassword, resetPassword, showResetPasswordDialog, resetting, }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 管理员密码修改组件 const PasswordChangeComponent = { template: `

管理员密码修改

确认修改 重置
`, setup() { const loading = ref(false); const oldPassword = ref(''); const newPassword = ref(''); const confirmPassword = ref(''); const passwordForm = ref(null); // 修改密码 const changePassword = async () => { if (!passwordForm.value.validate()) { return; } loading.value = true; fetch('/admin/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, body: JSON.stringify({ old_password: md5(oldPassword.value), new_password: md5(newPassword.value) }) }) .then(response => response.json()) .then((res) => { loading.value = false; if (data.code === 0) { resetForm(); snackbarStore.info('密码修改成功!'); } else { snackbarStore.error(res.message || '密码修改失败,请重试'); } }) .catch(err => { loading.value = false; console.error('修改密码失败:', err); snackbarStore.error(err.message || '密码修改失败,请重试'); }); }; // 重置表单 const resetForm = () => { oldPassword.value = ''; newPassword.value = ''; confirmPassword.value = ''; if (passwordForm.value) { passwordForm.value.reset(); } }; return { store, oldPassword, newPassword, confirmPassword, passwordForm, changePassword, resetForm, loading }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 问卷管理组件 const QuestionnaireComponent = { template: ` {{ editingQuestionnaire.id ? '编辑问卷' : '创建问卷' }} mdi-close取消 {{ saving ? '保存中...' : '保存' }} 问卷分类 题型选择 单选题 多选题 文本输入 滑动条 问卷介绍 问卷内容 维度管理 结果展示 主屏简介 副屏介绍
暂无问题,请点击左侧题型添加
{{ index + 1 }}. {{ questionTypeLabels[question.type] }}
选项 添加选项
选项 添加选项
滑动条设置
添加维度 注意:每个维度的管理在折叠面板中进行,点击维度名称可展开/折叠。 mdi-delete
mdi-book-open-page-variant

暂无维度数据

确认删除

确定要删除问卷 "{{ selectedQuestionnaire?.title }}" 吗? 取消 {{ deleting ? '删除中...' : '删除' }}
`, setup() { // 选项卡相关 const activeTab = ref("desc"); // 问卷列表相关 const questionnaires = ref([]); const categories = ref([]); const loading = ref(false); const saving = ref(false); const deleting = ref(false); // 对话框状态 const showEditDialog = ref(false); const showDeleteDialog = ref(false); // 选中的问卷 const selectedQuestionnaire = ref(null); // 题型标签映射 const questionTypeLabels = { single: '单选题', multiple: '多选题', text: '文本输入', slider: '滑动条' }; // 编辑中的问卷 - 基于模型结构 const editingQuestionnaire = reactive({ id: '', title: '', description: '', category_id: '', category_name: '', questions: [], dimensions: [], score_infos: [], tips: '', //副屏 fit_tips: '', notice_tips: '', reference_tips: '', detail_tips: '', scoring_mode: '', }); // 计算属性:当前维度的分数段 - 已移除,直接使用dimension.score_infos // 表格头部定义 const questionnaireHeaders = ref([ { title: '问卷名称', key: 'title' }, { title: '描述', key: 'description' }, { title: '所属分类', key: 'category_name' }, { title: '操作', key: 'actions', sortable: false } ]); const dimensionHeaders = ref([ { title: '维度名称', key: 'name', width: '60%' }, { title: '操作', key: 'actions', sortable: false, width: '40%' } ]); const scoreInfoHeaders = ref([ { title: '最小值', key: 'min_score', width: '12%' }, { title: '最大值', key: 'max_score', width: '12%' }, { title: '提示颜色', key: 'color', width: '10%' }, { title: '提示文字', key: 'prompt', width: '51%' }, { title: '操作', key: 'actions', sortable: false, width: '15%' } ]); const dimensionScoreHeaders = ref([ { title: '最小值', key: 'min_score', width: '12%' }, { title: '最大值', key: 'max_score', width: '12%' }, { title: '提示颜色', key: 'color', width: '10%' }, { title: '提示文字', key: 'prompt', width: '51%' }, { title: '操作', key: 'actions', sortable: false, width: '15%' } ]); // 加载分类列表 const loadCategories = async () => { fetch('/admin/categories', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` } }) .then((response) => response.json()) .then((data) => { if (data.code === 0) { categories.value = data.data || []; } else { snackbarStore.error(data.message || '获取分类列表失败'); } }) .catch((error) => { snackbarStore.error('网络错误,请稍后重试'); console.error('加载分类失败:', error); }); }; // 加载问卷列表 const loadQuestionnaires = async () => { loading.value = true; // 先加载分类 loadCategories().then(() => { // 加载问卷列表 fetch('/api/questionnaires', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, }) .then((response) => response.json()) .then((data) => { loading.value = false; if (data.code === 0) { questionnaires.value = data.data || []; } else { snackbarStore.error(data.message || '获取问卷列表失败'); } }).catch((error) => { questionnaires.value = []; loading.value = false; snackbarStore.error('网络错误,请稍后重试'); console.error('加载问卷失败:', error); }); }); }; // 刷新数据 const refreshData = () => { loadQuestionnaires(); }; // 创建新问卷 const createNewQuestionnaire = () => { // 重置编辑对象 Object.assign(editingQuestionnaire, { id: '', title: '', description: '', category_id: '', category_name: '', questions: [], dimensions: [], score_infos: [], tips: '', //副屏 fit_tips: '', notice_tips: '', reference_tips: '', detail_tips: '', scoring_mode: '', }); activeTab.value = "content"; showEditDialog.value = true; }; // 编辑问卷 const editQuestionnaire = async (questionnaireId) => { loading.value = true; // 加载问卷详情 fetch(`/admin/questionnaires/${questionnaireId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, }) .then((response) => response.json()) .then((data) => { loading.value = false; if (data.code === 0) { //对比是否有所有字段,没有则添加默认值。数组字段默认值为空数组。 data.data.questions = data.data.questions || []; data.data.dimensions = data.data.dimensions || []; data.data.score_infos = data.data.score_infos || []; // 为问题添加默认的反向计分字段 data.data.questions.forEach(q => { if (q.reverse_scoring === undefined) { q.reverse_scoring = false; } }); // 为维度添加默认的计分方式 data.data.dimensions.forEach(d => { if (d.scoring_mode === undefined) { d.scoring_mode = 'sum'; } }); Object.assign(editingQuestionnaire, data.data || {}); activeTab.value = "content"; showEditDialog.value = true; } else { snackbarStore.error(data.message || '获取问卷详情失败'); showEditDialog.value = false; } }).catch((error) => { loading.value = false; console.error('加载问卷详情失败:', error); snackbarStore.error('加载问卷详情失败,请重试'); showEditDialog.value = false; }).finally(() => { loading.value = false; }); }; // 添加问题 const addQuestion = (type) => { const newQuestion = { id: `q${Date.now()}`, title: '', description: '', type: type, dimension_id: editingQuestionnaire.dimensions.length > 0 ? editingQuestionnaire.dimensions[0].id : '', options: [], reverse_scoring: false }; // 为单选和多选题添加默认选项 if (type === 'single' || type === 'multiple') { newQuestion.options.push( { value: '1', text: '选项1', score: 0 }, ); } else if (type === 'slider') { newQuestion.min = 0; newQuestion.max = 100; newQuestion.step = 1; } editingQuestionnaire.questions.push(newQuestion); // 自动切换到问卷内容选项卡 activeTab.value = "content"; //this.$forceUpdate(); }; // 添加选项 const addOption = (question) => { const optionValue = (question.options.length + 1).toString(); question.options.push({ value: optionValue, text: `选项${optionValue}`, score: 0 }); }; // 删除选项 const deleteOption = (question, index) => { if (question.options.length > 1) { question.options.splice(index, 1); // 重新编号选项 question.options.forEach((opt, idx) => { opt.value = (idx + 1).toString(); }); } else { snackbarStore.warning('至少保留一个选项'); } }; // 删除问题 const deleteQuestion = (index) => { if (confirm('确定要删除这个问题吗?')) { editingQuestionnaire.questions.splice(index, 1); snackbarStore.success('问题删除成功'); } }; // 移动问题上移 const moveQuestionUp = (index) => { if (index > 0) { const temp = editingQuestionnaire.questions[index]; editingQuestionnaire.questions[index] = editingQuestionnaire.questions[index - 1]; editingQuestionnaire.questions[index - 1] = temp; } }; // 移动问题下移 const moveQuestionDown = (index) => { if (index < editingQuestionnaire.questions.length - 1) { const temp = editingQuestionnaire.questions[index]; editingQuestionnaire.questions[index] = editingQuestionnaire.questions[index + 1]; editingQuestionnaire.questions[index + 1] = temp; } }; // 添加维度 const addDimension = () => { editingQuestionnaire.dimensions.push({ id: `d${Date.now()}`, name: '', score_infos: [], scoring_mode: 'sum' }); }; // 删除维度 const deleteDimension = (dimension) => { if (confirm(`确定要删除维度 "${dimension.name}" 吗?`)) { const index = editingQuestionnaire.dimensions.findIndex(d => d.id === dimension.id); if (index > -1) { // 更新使用该维度的问题 editingQuestionnaire.questions.forEach(question => { if (question.dimension_id === dimension.id) { question.dimension_id = editingQuestionnaire.dimensions.length > 1 ? editingQuestionnaire.dimensions[index === 0 ? 1 : 0].id : ''; } }); editingQuestionnaire.dimensions.splice(index, 1); snackbarStore.success('维度删除成功'); } } }; // 显示维度分数段管理对话框 - 已移除,功能已整合到v-expansion-panel中 // 添加维度分数段 const addDimensionScore = (dimension) => { if (!dimension?.score_infos) { dimension.score_infos = []; } let nextMinScore = 0; let nextMaxScore = 20; if (dimension.score_infos.length > 0) { // 找到当前最大的 max_score,在其基础上+1 const maxMaxScore = Math.max(...dimension.score_infos.map(s => s.max_score || s.score || 0)); nextMinScore = maxMaxScore + 1; nextMaxScore = nextMinScore + 20; } dimension.score_infos.push({ id: `ds${Date.now()}`, min_score: nextMinScore, max_score: nextMaxScore, color: ['#4CAF50', '#8BC34A', '#FFC107', '#FF9800', '#FF5722'][dimension.score_infos.length % 5], prompt: '请输入提示文字' }); }; // 删除维度分数段 const deleteDimensionScore = (dimension, scoreInfo) => { if (confirm(`确定要删除区间 [${scoreInfo.min_score || scoreInfo.score}-${scoreInfo.max_score || scoreInfo.score}] 的分数段吗?`)) { const index = dimension.score_infos.findIndex(s => s.id === scoreInfo.id); if (index > -1) { dimension.score_infos.splice(index, 1); snackbarStore.success('分数段删除成功'); } } }; // 添加问卷分数段 const addScoreInfo = () => { let nextMinScore = 0; let nextMaxScore = 20; if (editingQuestionnaire.score_infos.length > 0) { // 找到当前最大的 max_score,在其基础上+1 const maxMaxScore = Math.max(...editingQuestionnaire.score_infos.map(s => s.max_score || s.score || 0)); nextMinScore = maxMaxScore + 1; nextMaxScore = nextMinScore + 20; } editingQuestionnaire.score_infos.push({ id: `si${Date.now()}`, min_score: nextMinScore, max_score: nextMaxScore, color: ['#4CAF50', '#8BC34A', '#FFC107', '#FF9800', '#FF5722'][editingQuestionnaire.score_infos.length % 5], prompt: '请输入提示文字' }); }; // 删除问卷分数段 const deleteScoreInfo = (scoreInfo) => { if (confirm(`确定要删除区间 [${scoreInfo.min_score || scoreInfo.score}-${scoreInfo.max_score || scoreInfo.score}] 的分数段吗?`)) { const index = editingQuestionnaire.score_infos.findIndex(s => s.id === scoreInfo.id); if (index > -1) { editingQuestionnaire.score_infos.splice(index, 1); snackbarStore.success('分数段删除成功'); } } }; // 保存问卷 const saveQuestionnaire = async () => { // 验证表单 if (!editingQuestionnaire.title.trim()) { snackbarStore.warning('请输入问卷标题'); return; } if (!editingQuestionnaire.category_id) { snackbarStore.warning('请选择问卷分类'); return; } if (editingQuestionnaire.questions.length === 0) { snackbarStore.warning('请至少添加一个问题'); return; } if (!editingQuestionnaire.tips.trim()) { snackbarStore.warning('请输入问卷结果提示'); return; } // 验证副屏提示 if (!editingQuestionnaire.detail_tips.trim()) { snackbarStore.warning('请输入副屏介绍'); return; } // 验证每个问题 for (let i = 0; i < editingQuestionnaire.questions.length; i++) { const question = editingQuestionnaire.questions[i]; if (!question.title.trim()) { snackbarStore.warning(`第 ${i + 1} 个问题的标题不能为空`); return; } if (!question.dimension_id) { snackbarStore.warning(`第 ${i + 1} 个问题请选择所属维度`); return; } // 验证单选和多选题的选项 if ((question.type === 'single' || question.type === 'multiple') && question.options.length === 0) { snackbarStore.warning(`第 ${i + 1} 个问题请至少添加一个选项`); return; } // 验证滑动条设置 if (question.type === 'slider') { if (question.min === undefined || question.max === undefined || question.step === undefined) { snackbarStore.warning(`第 ${i + 1} 个问题请完善滑动条设置`); return; } if (question.min >= question.max) { snackbarStore.warning(`第 ${i + 1} 个问题的最大值必须大于最小值`); return; } } } // 验证维度数据 if (editingQuestionnaire.dimensions.length === 0) { snackbarStore.warning('请至少添加一个维度'); return; } for (let i = 0; i < editingQuestionnaire.dimensions.length; i++) { const dimension = editingQuestionnaire.dimensions[i]; if (!dimension.name.trim()) { snackbarStore.warning(`第 ${i + 1} 个维度的名称不能为空`); return; } } saving.value = true; let rsp = null; if(!editingQuestionnaire.id || editingQuestionnaire.id.trim().length < 1){ rsp = fetch(`/admin/questionnaires/create`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `${store.token}` }, body: JSON.stringify(editingQuestionnaire), }); }else{ rsp = fetch(`/admin/questionnaires/${editingQuestionnaire.id}`, { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": `${store.token}` }, body: JSON.stringify(editingQuestionnaire), }); console.log(editingQuestionnaire); } rsp.then((response) => response.json()) .then((data) => { saving.value = false; if (data.code === 0) { if (editingQuestionnaire.id) { // 编辑模式 snackbarStore.success('问卷更新成功'); } else { // 创建模式 snackbarStore.success('问卷创建成功'); } showEditDialog.value = false; refreshData(); } else { snackbarStore.error(data.message || '获取问卷详情失败'); //showEditDialog.value = false; } }).catch((error) => { console.error('保存问卷失败:', error); snackbarStore.error('保存问卷失败,请重试'); }).finally(() => { saving.value = false; }) }; // 确认删除问卷 const confirmDeleteQuestionnaire = (questionnaire) => { selectedQuestionnaire.value = questionnaire; showDeleteDialog.value = true; }; // 删除问卷 const deleteQuestionnaire = async () => { deleting.value = true; showDeleteDialog.value = false; fetch(`/admin/questionnaires/${selectedQuestionnaire.value.id}`, { method: "DELETE", headers: { "Content-Type": "application/json", "Authorization": `${store.token}` }, }).then((response) => response.json()) .then((data) => { deleting.value = false; if (data.code === 0) { snackbarStore.success('问卷删除成功'); showDeleteDialog.value = false; refreshData(); } else { snackbarStore.error(data.message || '删除问卷失败'); } }).catch((error) => { deleting.value = false; console.error('删除问卷失败:', error); snackbarStore.error('删除问卷失败,请重试'); }).finally(() => { deleting.value = false; }) }; // 初始加载 loadQuestionnaires(); return { questionnaires, categories, loading, saving, deleting, showEditDialog, showDeleteDialog, selectedQuestionnaire, editingQuestionnaire, activeTab, questionTypeLabels, questionnaireHeaders, dimensionHeaders, scoreInfoHeaders, dimensionScoreHeaders, refreshData, editQuestionnaire, saveQuestionnaire, confirmDeleteQuestionnaire, deleteQuestionnaire, addDimension, deleteDimension, addDimensionScore, deleteDimensionScore, addScoreInfo, deleteScoreInfo, addQuestion, addOption, deleteOption, deleteQuestion, moveQuestionUp, moveQuestionDown, createNewQuestionnaire, loadQuestionnaires }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- //报告列表管理 const AnswersComponent = { template: ` mdi-close 报告详情 - {{ selectedAnswer?.questionnaire_name }}

确认删除

确定要删除报告 "{{ selectedAnswer?.title }}" 用户:{{ selectedAnswer?.nick_name}} 吗?此操作不可撤销。 取消 {{ deleting ? '删除中...' : '删除' }}

导出报告

取消 {{ exporting ? '导出中...' : '导出' }}
`, setup() { const loading = ref(false); const deleting = ref(false); const exporting = ref(false); const answers = ref([]); const questionnaires = ref([]); const selectedAnswer = ref(null); const selectedAnswerDetails = ref([]); const showDetailDialog = ref(false); const showDeleteDialog = ref(false); const showExportDialog = ref(false); const startDate = ref(''); const endDate = ref(''); const selectedQuestionnaireIds = ref([]); const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); const headers = [ //{ title: '报告ID', key: 'id', width: '100px' }, { title: '问卷名称', key: 'title' }, { title: '用户', key: 'nick_name' }, { title: '问卷分类', key: 'category_name' }, { title: '提交时间', key: 'created_at',value: (row) => toTime(row.created_at) }, { title: '操作', key: 'actions', width: '180px' } ]; const detailHeaders = [ { title: '题目', key: 'title' }, { title: '维度', key: 'dimension_name' }, { title: '得分', key: 'score' }, { title: '回答', key: 'value' } ]; // 加载问卷列表 const loadQuestionnaires = async () => { fetch('/api/questionnaires', { method: 'post', headers: { 'Authorization': `${store.token}`, 'Content-Type': 'application/json' } }).then((response) => response.json()) .then((data) => { if (data.code === 0) { questionnaires.value = data.data; } else { snackbarStore.error('加载问卷列表失败:' + data.msg); } }).catch((error) => { snackbarStore.error('加载问卷列表失败:' + error.message); }); }; // 加载报告列表 const loadAnswers = async () => { loading.value = true; fetch("/admin/reports", { method: 'post', headers: { 'Authorization': `${store.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ page: pagination.page, pageSize: pagination.pageSize, }) }) .then(response => response.json()) .then(data => { if (data.code === 0) { answers.value = data.data || []; pagination.total = data.total; } else { snackbarStore.error('加载报告列表失败:' + data.message); } loading.value = false; }) .catch(error => { loading.value = false; snackbarStore.error('加载报告列表失败:' + error.message); }) .finally(() => { loading.value = false; }); }; // 刷新数据 const refreshData = () => { pagination.page = 1; loadAnswers(); }; // 重置搜索 const resetSearch = () => { startDate.value = ''; endDate.value = ''; pagination.page = 1; loadAnswers(); }; // 处理页码变化 const handlePageChange = (page) => { pagination.page = page; loadAnswers(); }; // 处理每页条数变化 const handlePageSizeChange = (pageSize) => { pagination.pageSize = pageSize; pagination.page = 1; loadAnswers(); }; // 查看报告详情 const viewAnswer = async (answerId) => { loading.value = true; fetch(`/admin/reports/${answerId}`, { method: 'POST', headers: { 'Authorization': `${store.token}`, 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => { console.log(data); if (data.code === 0) { selectedAnswerDetails.value = data.data; showDetailDialog.value = true; } else { snackbarStore.error('加载报告详情失败:' + data.message); } loading.value = false; }).catch(error => { loading.value = false; snackbarStore.error('加载报告详情失败:' + error.message); }); }; // 确认删除 const confirmDelete = (answer) => { selectedAnswer.value = answer; showDeleteDialog.value = true; }; // 删除报告 const deleteAnswer = async () => { if (!selectedAnswer.value) return; deleting.value = true; fetch(`/admin/reports/${selectedAnswer.value.id}`, { method: 'DELETE', headers: { 'Authorization': `${store.token}`, 'Content-Type': 'application/json' } }) .then((response) => response.json()) .then((data) => { deleting.value = false; if (data.code === 0) { snackbarStore.success('删除成功'); showDeleteDialog.value = false; loadAnswers(); } else { snackbarStore.error('删除失败:' + data.msg); } }).catch((error) => { deleting.value = false; snackbarStore.error('删除失败:' + error.message); }).finally(() => { deleting.value = false; }); }; // 导出报告 const exportAnswers = async () => { if (selectedQuestionnaireIds.value.length === 0) { snackbarStore.warning('请至少选择一个报告题目'); return; } exporting.value = true; fetch(`/admin/reports/export`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `${store.token}` }, body: JSON.stringify({ questionnaireIds: selectedQuestionnaireIds.value, startDate:startDate.value, endDate: endDate.value, }) }).then(async (response) => { if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `报告数据_${new Date().getTime()}.csv`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); snackbarStore.success('导出成功'); showExportDialog.value = false; } else { snackbarStore.error('导出失败'); } }).catch((error) => { snackbarStore.error('导出失败:' + error.message); }).finally(() => { exporting.value = false; }); }; // 初始化 onMounted(async () => { await loadQuestionnaires(); await loadAnswers(); }); return { loading, deleting, exporting, answers, startDate, endDate, questionnaires, selectedAnswer, selectedAnswerDetails, showDetailDialog, showDeleteDialog, showExportDialog, selectedQuestionnaireIds, pagination, headers, detailHeaders, refreshData, resetSearch, handlePageChange, handlePageSizeChange, viewAnswer, confirmDelete, deleteAnswer, exportAnswers }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- //分享管理 const SharesComponent = { template: `

确认删除

确定要删除分享记录 "{{ selectedShare?.questionnaire_name }}" 吗?此操作不可撤销。 取消 {{ deleting ? '删除中...' : '删除' }}
`, setup() { const loading = ref(false); const deleting = ref(false); const shares = ref([]); const questionnaires = ref([]); const selectedShare = ref(null); const showDeleteDialog = ref(false); const keyword = ref(''); const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); const headers = [ { title: '问卷名称', key: 'title' }, { title: '分享人', key: 'user_nickname' }, { title: '听力师', key: 'audiologist_nickname' }, { title: '分享时间', key: 'shared_at' }, { title: '得分', key: 'score' }, { title: '总分', key: 'max_score' }, { title: '操作', key: 'actions', width: '100px' } ]; // 加载分享列表 const loadShares = async () => { loading.value = true; const params = { page: pagination.page, pageSize: pagination.pageSize, keyword: keyword.value, }; fetch(`/admin/shares`, { method: 'POST', body: JSON.stringify(params), headers: { 'Authorization': `${store.token}`, 'Content-Type': 'application/json' } }) .then(res => res.json()) .then(data => { loading.value = false; if (data.code === 0) { shares.value = data.data || []; pagination.total = data.total || 0; } else { snackbarStore.error('加载分享列表失败:' + data.msg); } }) .catch(error => { loading.value = false; snackbarStore.error('加载分享列表失败:' + error.message); }); }; // 刷新数据 const refreshData = () => { pagination.page = 1; loadShares(); }; // 搜索分享 const searchShares = () => { pagination.page = 1; loadShares(); }; // 重置搜索 const resetSearch = () => { keyword.value = ''; pagination.page = 1; loadShares(); }; // 处理页码变化 const handlePageChange = (page) => { pagination.page = page; loadShares(); }; // 处理每页条数变化 const handlePageSizeChange = (pageSize) => { pagination.pageSize = pageSize; pagination.page = 1; loadShares(); }; // 确认删除 const confirmDelete = (share) => { selectedShare.value = share; showDeleteDialog.value = true; }; // 删除分享 const deleteShare = async () => { if (!selectedShare.value) return; deleting.value = true; console.log(selectedShare.value); fetch(`/admin/shares/${selectedShare.value.id}`, { method: 'DELETE', headers: { 'Authorization': `${store.token}`, 'Content-Type': 'application/json' } }) .then(res => res.json()) .then(data => { console.log(data); deleting.value = false; showDeleteDialog.value = false; if (data.code === 0) { snackbarStore.success('删除成功'); showDeleteDialog.value = false; loadShares(); } else { snackbarStore.error('删除失败:' + data.message); } }) .catch(error => { showDeleteDialog.value = false; selectedShare.value = null; deleting.value = false; snackbarStore.error('删除失败:' + error.message); }) }; // 初始化 onMounted(async () => { await loadShares(); }); return { loading, deleting, shares, questionnaires, selectedShare, showDeleteDialog, keyword, pagination, headers, refreshData, searchShares, resetSearch, handlePageChange, handlePageSizeChange, confirmDelete, deleteShare }; } }; //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 创建路由 const router = createRouter({ history: createWebHistory('/'), routes: [ { path: '/login', component: LoginComponent, meta: { requiresAuth: false } }, { path: '/', component: DashboardComponent, meta: { requiresAuth: true } }, { path: '/users', component: UsersComponent, meta: { requiresAuth: true } }, // 添加更多路由 { path: '/questionnaire-categories', component: QuestionnaireCategoriesComponent, meta: { requiresAuth: true } }, { path: '/questionnaires', component: QuestionnaireComponent, meta: { requiresAuth: true } }, { path: '/audiologists', component: AudiologistsComponent, meta: { requiresAuth: true } }, { path: '/answers', component: AnswersComponent, meta: { requiresAuth: true } }, { path: '/shares', component: SharesComponent, meta: { requiresAuth: true } }, { path: '/settings', component: PasswordChangeComponent, meta: { requiresAuth: true } }, { path: '/:pathMatch(.*)*', redirect: '/' // 重定向到首页 } ] }); // 路由守卫 router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !store.isLoggedIn) { // 如果需要认证但未登录,重定向到登录页 next('/login'); } else { next(); } }); //------------------------------------------------------------------------------------------------------------------------------------------------------------- // 创建应用实例 const app = createApp({ template: ` `, setup() { // 检查登录状态 const checkLoginStatus = () => { if (store.checkLogin()) { store.isLoggedIn = true; } }; // 侧边栏导航项 const navItems = [ { title: '首页', icon: 'mdi-home', path: '/' }, { title: '问卷分类管理', icon: 'mdi-google-circles-extended', path: '/questionnaire-categories' }, { title: '问卷管理', icon: 'mdi-file-document', path: '/questionnaires' }, { title: '用户管理', icon: 'mdi-account-group', path: '/users' }, { title: '听力师管理', icon: 'mdi-stethoscope', path: '/audiologists' }, { title: '答卷列表', icon: 'mdi-chart-pie', path: '/answers' }, { title: '分享记录', icon: 'mdi-share-variant', path: '/shares' }, { title: '管理员设置', icon: 'mdi-cog', path: '/settings' }, { title: '退出登录', icon: 'mdi-logout', action: () => store.logout() } ]; // 侧边栏分组状态 const activeGroup = ref([]); // 切换分组展开/收起状态 const toggleGroup = (groupTitle) => { const index = activeGroup.value.indexOf(groupTitle); if (index > -1) { activeGroup.value.splice(index, 1); } else { activeGroup.value.push(groupTitle); } }; // 检查分组是否展开 const isGroupOpen = (groupTitle) => { return activeGroup.value.includes(groupTitle); }; // 初始化时检查登录状态 checkLoginStatus(); return { snackbarStore, isLoggedIn: computed(() => store.isLoggedIn), username: computed(() => store.userInfo.username || store.userInfo.nickname), navItems, activeGroup, toggleGroup, isGroupOpen }; } }); // 全局可用的通知函数 //app.config.globalProperties.$notify = showNotification; // 注册全局组件 app.component('ColorPickerButton', ColorPickerButton); app.component('DatePickerDialog', DatePickerDialog); // 使用插件 app.use(vuetify); app.use(router); // 挂载应用 app.mount('#app'); // 添加淡入淡出过渡样式和ColorPickerButton组件样式 const style = document.createElement('style'); style.textContent = ` .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } `; document.head.appendChild(style);