diff --git a/.env.production b/.env.production index 7a81ebe..5317c51 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,4 @@ # 生产构建默认 API(与 szkp-map-h5 同域部署) # 注意:.env.local 会覆盖本文件,请用 npm run build:prod 或去掉 .env.local 里的 VITE_API_BASE_URL 后再 build VITE_API_BASE_URL=https://szkp-map.langye.net/api +VITE_PEOPLE_COUNTING_URL=https://hik.pdc.langye.net:18080/api/people-counting diff --git a/.env.testing b/.env.testing index afb28d7..e61580e 100644 --- a/.env.testing +++ b/.env.testing @@ -1,3 +1,4 @@ VITE_API_BASE_URL=https://szkp-map.langye.net/api VITE_TENCENT_MAP_KEY=CRFBZ-NTART-YU4XX-LCDGK-3J456-VKBK2 VITE_TENCENT_MAP_REFERER=szkp-map-admin +VITE_PEOPLE_COUNTING_URL=https://hik.pdc.langye.net:18080/api/people-counting diff --git a/src/api/http.ts b/src/api/http.ts index 9f1e1d3..8fa87c4 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -1,4 +1,5 @@ -import axios from 'axios' +import axios, { AxiosError } from 'axios' +import { Message } from '@arco-design/web-vue' export const TOKEN_KEY = 'szkp_admin_token' @@ -9,6 +10,12 @@ export const http = axios.create({ timeout: 10000, }) +function loginPath() { + const base = import.meta.env.BASE_URL || '/' + const normalized = base.endsWith('/') ? base : `${base}/` + return `${normalized}login` +} + http.interceptors.request.use((config) => { const token = localStorage.getItem(TOKEN_KEY) if (token) { @@ -17,3 +24,21 @@ http.interceptors.request.use((config) => { return config }) +http.interceptors.response.use( + (res) => res, + (error: AxiosError) => { + const status = error.response?.status + const url = String(error.config?.url ?? '') + const isLoginRequest = url.includes('/auth/login') + + if (status === 401 && !isLoginRequest) { + localStorage.removeItem(TOKEN_KEY) + Message.warning('登录已过期,请重新登录') + const next = `${loginPath()}?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}` + window.location.replace(next) + } + + return Promise.reject(error) + }, +) + diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index e1b6b28..e67b8df 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -15,8 +15,10 @@ type MenuItem = { children?: MenuItem[] } type CurrentUser = { + id?: number name?: string username?: string + role?: string } const router = useRouter() @@ -25,6 +27,13 @@ const route = useRoute() const collapsed = ref(false) const menu = ref([]) const currentUser = ref(null) +const profileVisible = ref(false) +const profileSaving = ref(false) +const profileForm = ref({ + name: '', + password: '', + confirmPassword: '', +}) const activePath = computed(() => route.path) @@ -69,6 +78,56 @@ async function loadCurrentUser() { } } +function openProfileModal() { + profileForm.value = { + name: currentUser.value?.name || '', + password: '', + confirmPassword: '', + } + profileVisible.value = true +} + +function validPassword(pwd: string): boolean { + if (!pwd) return true + if (pwd.length < 8) return false + if (!/[a-z]/.test(pwd)) return false + if (!/[A-Z]/.test(pwd)) return false + if (!/[^A-Za-z0-9]/.test(pwd)) return false + return true +} + +async function submitProfile() { + const name = profileForm.value.name.trim() + if (!name) { + Message.warning('姓名不能为空') + return false + } + if (profileForm.value.password && !validPassword(profileForm.value.password)) { + Message.warning('密码需包含大小写字母和特殊字符,且长度至少 8 位') + return false + } + if (profileForm.value.password !== profileForm.value.confirmPassword) { + Message.warning('两次输入的密码不一致') + return false + } + + profileSaving.value = true + try { + const payload: { name: string; password?: string } = { name } + if (profileForm.value.password) payload.password = profileForm.value.password + const { data } = await http.put('/me/profile', payload) + currentUser.value = { ...(currentUser.value || {}), ...data } + Message.success('个人信息已更新') + profileVisible.value = false + return true + } catch (error: any) { + Message.error(error?.response?.data?.message ?? '保存失败') + return false + } finally { + profileSaving.value = false + } +} + async function logout() { try { await http.post('/auth/logout') @@ -135,6 +194,7 @@ onMounted(async () => {
{{ route.meta?.title ?? '后台管理' }}
{{ currentUser?.name || currentUser?.username || '未登录' }} + 修改资料 退出登录
@@ -144,6 +204,26 @@ onMounted(async () => { + + + + + + + + + + + + + + diff --git a/src/views/activities/Blacklist.vue b/src/views/activities/Blacklist.vue index 23edaaa..a911dae 100644 --- a/src/views/activities/Blacklist.vue +++ b/src/views/activities/Blacklist.vue @@ -176,11 +176,10 @@ onMounted(async () => {
- + {{ v.name }} 查询 - 刷新 批量移出灰名单
diff --git a/src/views/study-tours/StudyTourList.vue b/src/views/study-tours/StudyTourList.vue index b7c9480..40db697 100644 --- a/src/views/study-tours/StudyTourList.vue +++ b/src/views/study-tours/StudyTourList.vue @@ -13,6 +13,10 @@ type Venue = { name: string } +type CurrentUser = { + role: string +} + type StudyTour = { id: number name: string @@ -31,6 +35,7 @@ const isCreate = ref(true) const editId = ref(null) const rows = ref([]) const venues = ref([]) +const currentUser = ref(null) const filterKeyword = ref('') const filterVenueId = ref(undefined) const filterOnShelf = ref(undefined) @@ -215,12 +220,13 @@ function onImageLoadError(ev?: Event) { Message.error('图片地址无法访问,请检查后端 storage 访问配置') } -function resetFilters() { - filterKeyword.value = '' - filterVenueId.value = undefined - filterOnShelf.value = undefined - listPagination.current = 1 - void loadData() +function isVenueAdmin() { + return currentUser.value?.role === 'venue_admin' +} + +async function loadMe() { + const { data } = await http.get('/me') + currentUser.value = data } async function loadData(keepCurrentPage = false) { @@ -391,7 +397,10 @@ async function removeRow(row: StudyTour) { } } -onMounted(loadData) +onMounted(async () => { + await loadMe().catch(() => undefined) + await loadData() +}) - - 场馆管理员账号仅能看到和编辑自己绑定的场馆;超级管理员可管理全部场馆。 - - { - - - - 请先下载模板,按「选项」工作表中的下拉值填写;多个主题用英文逗号分隔。封面与轮播图不在导入范围内,请在列表中编辑上传。经纬度可留空,导入后请在列表编辑中通过「地图选点」填写;经度与纬度须成对填写或同时留空。 - - - 下载导入模板 - - - - - - - - @@ -1718,29 +1372,6 @@ onMounted(async () => {