运行lint,将theLastPage的内容存储在sessionStorage里

This commit is contained in:
Linus Torvalds
2026-02-27 19:38:16 +08:00
parent bd1d9f2cec
commit 1f8a5f1d98
40 changed files with 663 additions and 514 deletions

3
front/src/app.d.ts vendored
View File

@@ -10,7 +10,4 @@ declare global {
} }
} }
export {}; export {};

View File

@@ -93,7 +93,7 @@ export async function GetTraces(r: string): Promise<GetTracesRes> {
return res.data; return res.data;
} }
export async function TicketOverview(): Promise<TicketOverviewRes>{ export async function TicketOverview(): Promise<TicketOverviewRes> {
const res = await api.get('/api/v3/ticket_overview'); const res = await api.get('/api/v3/ticket_overview');
return res.data; return res.data;
} }

View File

@@ -4,9 +4,9 @@
import Settings from 'carbon-icons-svelte/lib/Settings.svelte'; import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
import Home from 'carbon-icons-svelte/lib/Home.svelte'; import Home from 'carbon-icons-svelte/lib/Home.svelte';
import Return from 'carbon-icons-svelte/lib/Return.svelte'; import Return from 'carbon-icons-svelte/lib/Return.svelte';
import SearchAdvanced from "carbon-icons-svelte/lib/SearchAdvanced.svelte"; import SearchAdvanced from 'carbon-icons-svelte/lib/SearchAdvanced.svelte';
import CarbonForIbmDotcom from "carbon-icons-svelte/lib/CarbonForIbmDotcom.svelte"; import CarbonForIbmDotcom from 'carbon-icons-svelte/lib/CarbonForIbmDotcom.svelte';
import EventSchedule from "carbon-icons-svelte/lib/EventSchedule.svelte"; import EventSchedule from 'carbon-icons-svelte/lib/EventSchedule.svelte';
import { page } from '$app/state'; import { page } from '$app/state';
let isAdminView = $derived(page.url.pathname.startsWith('/admin')); let isAdminView = $derived(page.url.pathname.startsWith('/admin'));
@@ -45,12 +45,15 @@
<span>后台中心</span> <span>后台中心</span>
</a> </a>
<a href="/op/ticket_search" class="nav-item" class:active={page.url.pathname === '/op/ticket_search'}> <a
href="/op/ticket_search"
class="nav-item"
class:active={page.url.pathname === '/op/ticket_search'}
>
<SearchAdvanced /> <SearchAdvanced />
<span>检索工单</span> <span>检索工单</span>
</a> </a>
<a href="/op/scheduler" class="nav-item" class:active={page.url.pathname === '/op/scheduler'}> <a href="/op/scheduler" class="nav-item" class:active={page.url.pathname === '/op/scheduler'}>
<EventSchedule /> <EventSchedule />
<span>我的排班</span> <span>我的排班</span>
</a> </a>
@@ -69,12 +72,19 @@
<span>管理中心</span> <span>管理中心</span>
</a> </a>
<a href="/admin/add_ticket" class="nav-item" class:active={page.url.pathname === '/admin/add_ticket'}> <a
href="/admin/add_ticket"
class="nav-item"
class:active={page.url.pathname === '/admin/add_ticket'}
>
<SearchAdvanced /> <SearchAdvanced />
<span>增添工单</span> <span>增添工单</span>
</a> </a>
<a href="/admin/scheduler" class="nav-item" class:active={page.url.pathname === '/admin/scheduler'}> <a
href="/admin/scheduler"
class="nav-item"
class:active={page.url.pathname === '/admin/scheduler'}
>
<EventSchedule /> <EventSchedule />
<span>成员排班</span> <span>成员排班</span>
</a> </a>

View File

@@ -1,5 +1,5 @@
<script> <script>
let { children,...rest } = $props(); let { children, ...rest } = $props();
</script> </script>
<div class="card" {...rest}> <div class="card" {...rest}>
@@ -54,7 +54,7 @@
color: #4a4949; color: #4a4949;
} }
.card :global(h2) { .card :global(h2) {
font-size: 25px; font-size: 25px;
} }
</style> </style>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { BlockMap } from '$lib/types/enum'; import { BlockMap } from '$lib/types/enum';
import type { WtsBlock } from '$lib/types/enum'; import type { WtsBlock } from '$lib/types/enum';
let { b,r }: { b: WtsBlock, r: string } = $props(); let { b, r }: { b: WtsBlock; r: string } = $props();
</script> </script>
<span> <span>
{BlockMap[b]}-{r} {BlockMap[b]}-{r}
</span> </span>

View File

@@ -31,7 +31,7 @@
{/if} {/if}
<div class="flex items-baseline" style="margin-top: 12.5px; font-size: 15.5px;"> <div class="flex items-baseline" style="margin-top: 12.5px; font-size: 15.5px;">
<strong style="flex-shrink: 0;width: 7em;">状态</strong> <strong style="flex-shrink: 0;width: 7em;">状态</strong>
<div style="font-size: 15px;"><WtsStatus s={t.status} ap={t.appointed_at}/></div> <div style="font-size: 15px;"><WtsStatus s={t.status} ap={t.appointed_at} /></div>
</div> </div>
<div class="flex items-baseline" style="margin-top: 12.5px; font-size: 15.5px;"> <div class="flex items-baseline" style="margin-top: 12.5px; font-size: 15.5px;">
<strong style="flex-shrink: 0;width: 7em;">报修时间</strong> <strong style="flex-shrink: 0;width: 7em;">报修时间</strong>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { AccessMap } from '$lib/types/enum'; import { AccessMap } from '$lib/types/enum';
import type { WtsAccess } from '$lib/types/enum'; import type { WtsAccess } from '$lib/types/enum';
let { a }: { a: WtsAccess } = $props(); let { a }: { a: WtsAccess } = $props();
</script> </script>
<span class="inline-block bg-gray-300 px-3 py-1 text-[15px] rounded-[19px]"> <span class="inline-block rounded-[19px] bg-gray-300 px-3 py-1 text-[15px]">
{AccessMap[a]} {AccessMap[a]}
</span> </span>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { CategoryMap } from '$lib/types/enum'; import { CategoryMap } from '$lib/types/enum';
import type { WtsCategory } from '$lib/types/enum'; import type { WtsCategory } from '$lib/types/enum';
let { c }: { c: WtsCategory } = $props(); let { c }: { c: WtsCategory } = $props();
</script> </script>
<span> <span>
{CategoryMap[c]} {CategoryMap[c]}
</span> </span>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { ISPMap } from '$lib/types/enum'; import { ISPMap } from '$lib/types/enum';
import type { WtsISP } from '$lib/types/enum'; import type { WtsISP } from '$lib/types/enum';
let { i }: { i: WtsISP } = $props(); let { i }: { i: WtsISP } = $props();
</script> </script>
<span> <span>
{ISPMap[i]} {ISPMap[i]}
</span> </span>

View File

@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import { PriorityMap } from '$lib/types/enum'; import { PriorityMap } from '$lib/types/enum';
import type { WtsPriority } from '$lib/types/enum'; import type { WtsPriority } from '$lib/types/enum';
let { p }: { p: WtsPriority } = $props(); let { p }: { p: WtsPriority } = $props();
</script> </script>
{#if p === 'highest' || p==='assigned'} {#if p === 'highest' || p === 'assigned'}
<span class="text-red-600" style="font-size: 15px;"> <span class="text-red-600" style="font-size: 15px;">
<strong>{PriorityMap[p]}</strong> <strong>{PriorityMap[p]}</strong>
</span> </span>
{/if} {/if}
{#if p!== 'highest' && p!=='assigned'} {#if p !== 'highest' && p !== 'assigned'}
<span class="text-gray-600" style="font-size: 15px;"> <span class="text-gray-600" style="font-size: 15px;">
<strong>{PriorityMap[p]}</strong> <strong>{PriorityMap[p]}</strong>
</span> </span>
{/if} {/if}

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { StatusMap } from '$lib/types/enum'; import { StatusMap } from '$lib/types/enum';
import type { WtsStatus } from '$lib/types/enum'; import type { WtsStatus } from '$lib/types/enum';
import { DateRFC3339, type RFC3339 } from '$lib/types/RFC3339'; import { DateRFC3339, type RFC3339 } from '$lib/types/RFC3339';
import { isSameDay} from 'date-fns'; import { isSameDay } from 'date-fns';
let { s, ap }: { s: WtsStatus; ap: RFC3339 } = $props(); let { s, ap }: { s: WtsStatus; ap: RFC3339 } = $props();
const colorMap: Record<WtsStatus, string> = { const colorMap: Record<WtsStatus, string> = {
@@ -14,15 +14,16 @@
canceled: 'text-gray-500' canceled: 'text-gray-500'
}; };
</script> </script>
{#if s === 'scheduled' && isSameDay(ap, new Date())} {#if s === 'scheduled' && isSameDay(ap, new Date())}
<span> <span>
<strong class="text-blue-600">已预约</strong> <strong class="text-blue-600">已预约</strong>
<strong class="text-red-600">(今天)</strong> <strong class="text-red-600">(今天)</strong>
</span> </span>
{:else} {:else}
<span class={colorMap[s]}> <span class={colorMap[s]}>
<strong>{StatusMap[s]}</strong> <strong>{StatusMap[s]}</strong>
</span> </span>
{/if} {/if}
<style> <style>

View File

@@ -32,7 +32,7 @@
{:else if src === 'operator'} {:else if src === 'operator'}
<!-- 后台工单记录视图下的下方按钮 --> <!-- 后台工单记录视图下的下方按钮 -->
<ModalFooter> <ModalFooter>
<Button kind="secondary" on:click={() => ((open = false), (view = 'trace'))}>返回 </Button> <Button kind="secondary" on:click={() => ((open = false), (view = 'trace'))}>返回</Button>
<Button kind="primary" on:click={() => (view = 'update')}>更新状态</Button> <Button kind="primary" on:click={() => (view = 'update')}>更新状态</Button>
</ModalFooter> </ModalFooter>
{/if} {/if}

View File

@@ -25,7 +25,12 @@
open = $bindable(TicketModal.Opened), open = $bindable(TicketModal.Opened),
src = TicketModal.SRC, src = TicketModal.SRC,
onTicketChanged onTicketChanged
}: { t?: Ticket | null; open?: boolean; src?: 'user' | 'operator'; onTicketChanged?: () => void | Promise<void>;} = $props(); }: {
t?: Ticket | null;
open?: boolean;
src?: 'user' | 'operator';
onTicketChanged?: () => void | Promise<void>;
} = $props();
let loading = $state(false); let loading = $state(false);
let traces: Trace[] = $state([]); let traces: Trace[] = $state([]);
@@ -129,7 +134,15 @@
<NotificationQueue bind:this={q1} /> <NotificationQueue bind:this={q1} />
<ModalHeader title=" 🖋请更新No.{getTid(t)}的状态" /> <ModalHeader title=" 🖋请更新No.{getTid(t)}的状态" />
<ModalBody> <ModalBody>
<TraceUpdateView {t} bind:view bind:open bind:isUpReady {q} {q1} onUpdated={onTicketChanged} /> <TraceUpdateView
{t}
bind:view
bind:open
bind:isUpReady
{q}
{q1}
onUpdated={onTicketChanged}
/>
</ModalBody> </ModalBody>
<UpViewButton bind:view bind:isUpReady /> <UpViewButton bind:view bind:isUpReady />
</ComposedModal> </ComposedModal>

View File

@@ -17,8 +17,8 @@
return ''; // 默认为蓝色 return ''; // 默认为蓝色
} }
function userOPName(op: string):string { function userOPName(op: string): string {
if (op === '用户' && src === 'user'){ if (op === '用户' && src === 'user') {
return '您'; return '您';
} }
return op; return op;

View File

@@ -2,16 +2,16 @@
import type { Ticket } from '$lib/types/apiResponse'; import type { Ticket } from '$lib/types/apiResponse';
import { ModalFooter, Button } from 'carbon-components-svelte'; import { ModalFooter, Button } from 'carbon-components-svelte';
let{ let {
view = $bindable<'trace' | 'cancel' | 'update'>(), view = $bindable<'trace' | 'cancel' | 'update'>(),
isUpReady = $bindable<boolean>(), isUpReady = $bindable<boolean>()
}:{ }: {
view: 'trace' | 'cancel' | 'update', view: 'trace' | 'cancel' | 'update';
isUpReady: boolean, isUpReady: boolean;
} = $props(); } = $props();
</script> </script>
<ModalFooter> <ModalFooter>
<Button kind="secondary" on:click={() => (view = 'trace')}>返回上一级</Button> <Button kind="secondary" on:click={() => (view = 'trace')}>返回上一级</Button>
<Button kind="primary" on:click={() =>(isUpReady=true)}>确认</Button> <Button kind="primary" on:click={() => (isUpReady = true)}>确认</Button>
</ModalFooter> </ModalFooter>

View File

@@ -91,7 +91,7 @@ export function Guard(a: (subject: WtsAccess) => boolean) {
return; return;
} }
if (!a(jwt.access)) { if (!a(jwt.access)) {
if(jwt.access === "unregistered"){ if (jwt.access === 'unregistered') {
goto('/register'); goto('/register');
return; return;
} }

View File

@@ -1,16 +1,30 @@
import { browser } from '$app/environment';
//全局状态保存用户刚刚访问的页面以便JWT获取后跳转回来 //全局状态保存用户刚刚访问的页面以便JWT获取后跳转回来
class theLastPage { class theLastPage {
p = $state('/'); p = $state('/');
constructor() {
if (browser) {
this.p = sessionStorage.getItem('_the_last_page') || '/';
}
}
Write(p: string) { Write(p: string) {
this.p = p; this.p = p;
if (browser) {
sessionStorage.setItem('_the_last_page', p);
}
} }
Read(): string { Read(): string {
const p1 = this.p; const p1 = this.p;
this.p = '/'; this.p = '/';
if (browser) {
sessionStorage.removeItem('_the_last_page');
}
return p1; return p1;
} }
} }
export const TheLastPage = new theLastPage(); export const TheLastPage = new theLastPage();

View File

@@ -2,48 +2,47 @@ import type { FilterTicketsReq } from '$lib/types/apiRequest';
import type { WtsZone } from '$lib/types/enum'; import type { WtsZone } from '$lib/types/enum';
export type Criteria = { export type Criteria = {
r: FilterTicketsReq; r: FilterTicketsReq;
_order: 'priority' | 'newest' | 'oldest'; _order: 'priority' | 'newest' | 'oldest';
_floor: number | null; _floor: number | null;
_blocks_in_zone: WtsZone[]; //需要和req.block保持一致注意 _blocks_in_zone: WtsZone[]; //需要和req.block保持一致注意
_view_today_scheduled: boolean; _view_today_scheduled: boolean;
}; };
export let criteria: Criteria = { export let criteria: Criteria = {
r: { r: {
scope: 'active', scope: 'active',
issuer: undefined, issuer: undefined,
block: [], block: [],
status: [], status: [],
priority: [], priority: [],
category: [], category: [],
isp: [], isp: [],
newer_than: undefined, newer_than: undefined,
older_than: undefined, older_than: undefined
}, },
_order: 'priority', _order: 'priority',
_floor: null, _floor: null,
_blocks_in_zone: [], _blocks_in_zone: [],
_view_today_scheduled: false _view_today_scheduled: false
} as Criteria; } as Criteria;
export function resetCriteria() { export function resetCriteria() {
criteria = { criteria = {
r: { r: {
scope: 'active', scope: 'active',
issuer: undefined, issuer: undefined,
block: [], block: [],
status: [], status: [],
priority: [], priority: [],
category: [], category: [],
isp: [], isp: [],
newer_than: undefined, newer_than: undefined,
older_than: undefined, older_than: undefined
}, },
_order: 'priority', _order: 'priority',
_floor: null, _floor: null,
_blocks_in_zone: [], _blocks_in_zone: [],
_view_today_scheduled: false _view_today_scheduled: false
} as Criteria; } as Criteria;
} }

View File

@@ -12,7 +12,7 @@ class TicketDetailState {
this.NowTicket = t; this.NowTicket = t;
this.Opened = true; this.Opened = true;
this.SRC = s; this.SRC = s;
} }
// 关闭详情页 // 关闭详情页
close() { close() {

View File

@@ -1,99 +1,99 @@
import type { Ticket, UserProfile, Trace } from '$lib/types/apiResponse'; import type { Ticket, UserProfile, Trace } from '$lib/types/apiResponse';
import { NowRFC3339, RFC3339 } from '$lib/types/RFC3339'; import { NowRFC3339, RFC3339 } from '$lib/types/RFC3339';
export const sample1issuer: UserProfile = { export const sample1issuer: UserProfile = {
sid: '2020123456', sid: '2020123456',
name: '张三', name: '张三',
block: 'XHA', block: 'XHA',
access: 'user', access: 'user',
room: '123', room: '123',
phone: '13800138000', phone: '13800138000',
isp: 'mobile', isp: 'mobile',
account: '12345678901@139.gd', account: '12345678901@139.gd',
wx: 'zhangsan_wx', wx: 'zhangsan_wx'
}; };
export const sample1: Ticket = { export const sample1: Ticket = {
tid: 3, tid: 3,
issuer: sample1issuer, issuer: sample1issuer,
description: '网络坏了啊啊啊,快来修!好像是网线被蟑螂咬坏了。', description: '网络坏了啊啊啊,快来修!好像是网线被蟑螂咬坏了。',
occur_at: RFC3339('2023-12-31T23:59:59Z'), occur_at: RFC3339('2023-12-31T23:59:59Z'),
submitted_at: RFC3339('2024-01-01T00:10:00Z'), submitted_at: RFC3339('2024-01-01T00:10:00Z'),
category: 'ip-or-device', category: 'ip-or-device',
status: 'scheduled', status: 'scheduled',
priority: 'assigned', priority: 'assigned',
appointed_at: NowRFC3339(), appointed_at: NowRFC3339()
}; };
export const sample2issuer: UserProfile = { export const sample2issuer: UserProfile = {
sid: '2020123456', sid: '2020123456',
name: 'hajimi', name: 'hajimi',
block: '17', block: '17',
access: 'user', access: 'user',
room: '701', room: '701',
phone: '13800138000', phone: '13800138000',
isp: 'telecom', isp: 'telecom',
account: '18923456789', account: '18923456789',
wx: 'zhangsan_wx', wx: 'zhangsan_wx'
}; };
export const sample2: Ticket = { export const sample2: Ticket = {
tid: 2, tid: 2,
issuer: sample2issuer, issuer: sample2issuer,
description: '才办的宽带,麻烦来装下,谢谢!', description: '才办的宽带,麻烦来装下,谢谢!',
occur_at: RFC3339('2023-12-31T23:59:59Z'), occur_at: RFC3339('2023-12-31T23:59:59Z'),
submitted_at: RFC3339('2024-01-01T00:10:00Z'), submitted_at: RFC3339('2024-01-01T00:10:00Z'),
category: 'first-install', category: 'first-install',
status: 'fresh', status: 'fresh',
priority: 'mainline', priority: 'mainline'
}; };
export const sampleTrace: Trace[] = [ export const sampleTrace: Trace[] = [
{ {
opid: 1, opid: 1,
tid: 10, tid: 10,
updated_at: RFC3339('2024-01-01T10:00:00Z'), updated_at: RFC3339('2024-01-01T10:00:00Z'),
op: '2395', op: '2395',
op_name: '(用户操作)', op_name: '(用户操作)',
remark: '工单已提交', remark: '工单已提交',
new_status: 'fresh', new_status: 'fresh',
new_priority: 'mainline', new_priority: 'mainline'
}, },
{ {
opid: 2, opid: 2,
tid: 10, tid: 10,
updated_at: RFC3339('2024-01-01T10:00:00Z'), updated_at: RFC3339('2024-01-01T10:00:00Z'),
op: '2395', op: '2395',
op_name: '哈哈哈', op_name: '哈哈哈',
remark: '用户预约了时间', remark: '用户预约了时间',
new_status: 'scheduled', new_status: 'scheduled',
new_appointment: RFC3339('2024-01-13T14:00:00Z'), new_appointment: RFC3339('2024-01-13T14:00:00Z')
}, },
{ {
opid: 3, opid: 3,
tid: 10, tid: 10,
updated_at: RFC3339('2024-01-01T10:00:00Z'), updated_at: RFC3339('2024-01-01T10:00:00Z'),
op: '2395', op: '2395',
op_name: '啊啊啊', op_name: '啊啊啊',
remark: '材料不足,改日修', remark: '材料不足,改日修',
new_status: 'delay', new_status: 'delay'
}, },
{ {
opid: 4, opid: 4,
tid: 10, tid: 10,
updated_at: RFC3339('2024-01-01T10:00:00Z'), updated_at: RFC3339('2024-01-01T10:00:00Z'),
op: '2395', op: '2395',
op_name: '嘿嘿嘿', op_name: '嘿嘿嘿',
remark: '与用户重新约定时间预约在2024-01-21下午', remark: '与用户重新约定时间预约在2024-01-21下午',
new_status: 'scheduled', new_status: 'scheduled',
new_appointment: RFC3339('2024-01-21T15:00:00Z'), new_appointment: RFC3339('2024-01-21T15:00:00Z')
}, },
{ {
opid: 5, opid: 5,
tid: 10, tid: 10,
updated_at: RFC3339('2024-01-01T10:00:00Z'), updated_at: RFC3339('2024-01-01T10:00:00Z'),
op: '2395', op: '2395',
op_name: '喵喵喵', op_name: '喵喵喵',
remark: '问题解决:用户路由器线路接触不良,更换后恢复正常', remark: '问题解决:用户路由器线路接触不良,更换后恢复正常',
new_status: 'solved', new_status: 'solved'
}, }
]; ];

View File

@@ -1,33 +1,81 @@
export type WtsBlock = export type WtsBlock =
| '1'| '2'| '3'| '4'| '5'| '6' //凤翔 | '1'
| '7'| '8'| '9'| '10'| '11' //北门 | '2'
| '12'| '13'| '14'| '15'| '20'| '21'| '22' //东门 | '3'
| '16'| '17'| '18'| '19' //岐头 | '4'
| 'XHA'| 'XHB'| 'XHC'| 'XHD' //香晖 | '5'
| 'ZH' //朝晖 | '6' //凤翔
| 'other'; | '7'
| '8'
| '9'
| '10'
| '11' //北门
| '12'
| '13'
| '14'
| '15'
| '20'
| '21'
| '22' //东门
| '16'
| '17'
| '18'
| '19' //岐头
| 'XHA'
| 'XHB'
| 'XHC'
| 'XHD' //香晖
| 'ZH' //朝晖
| 'other';
export const BlockMap: Record<WtsBlock, string> = { export const BlockMap: Record<WtsBlock, string> = {
'1':'1栋','2':'2栋','3':'3栋','4':'4栋','5':'5栋','6':'6栋', '1': '1栋',
'7':'7栋','8':'8栋','9':'9栋','10':'10栋','11':'11栋', '2': '2栋',
'12':'12栋','13':'13栋','14':'14栋','15':'15栋','20':'20栋','21':'21栋','22':'22栋', '3': '3栋',
'16':'16栋','17':'17栋','18':'18栋','19':'19栋', '4': '4栋',
'XHA':'香晖A','XHB':'香晖B','XHC':'香晖C','XHD':'香晖D', '5': '5栋',
'ZH':'朝晖', '6': '6栋',
'other':'其它', '7': '7栋',
} '8': '8栋',
'9': '9栋',
'10': '10栋',
'11': '11栋',
'12': '12栋',
'13': '13栋',
'14': '14栋',
'15': '15栋',
'20': '20栋',
'21': '21栋',
'22': '22栋',
'16': '16栋',
'17': '17栋',
'18': '18栋',
'19': '19栋',
XHA: '香晖A',
XHB: '香晖B',
XHC: '香晖C',
XHD: '香晖D',
ZH: '朝晖',
other: '其它'
};
export type WtsAccess = export type WtsAccess =
| 'dev' | 'dev'
| 'chief'| 'api'| 'group-leader' | 'chief'
| 'formal-member'| 'informal-member' | 'api'
| 'pre-member'| 'user' | 'group-leader'
| 'unregistered'; | 'formal-member'
| 'informal-member'
| 'pre-member'
| 'user'
| 'unregistered';
export const IsAccessIn =
(...targets: WtsAccess[]) =>
(subject: WtsAccess): boolean => {
return targets.includes(subject);
};
export const IsAccessIn = (...targets: WtsAccess[]) => (subject: WtsAccess): boolean => {
return targets.includes(subject);
}
export const IsOperator = IsAccessIn( export const IsOperator = IsAccessIn(
'api', 'api',
'chief', 'chief',
@@ -36,14 +84,9 @@ export const IsOperator = IsAccessIn(
'formal-member', 'formal-member',
'informal-member' 'informal-member'
); );
export const IsAdmin = IsAccessIn( export const IsAdmin = IsAccessIn('group-leader', 'api', 'chief', 'dev');
'group-leader',
'api',
'chief',
'dev'
);
export const IsUser = IsAccessIn( export const IsUser = IsAccessIn(
'api', 'api',
'chief', 'chief',
@@ -54,111 +97,132 @@ export const IsUser = IsAccessIn(
'pre-member', 'pre-member',
'user' 'user'
); );
export const IsPreMember = IsAccessIn('pre-member'); export const IsPreMember = IsAccessIn('pre-member');
export const IsFormalMember = IsAccessIn( export const IsFormalMember = IsAccessIn('group-leader', 'api', 'chief', 'dev', 'formal-member');
'group-leader',
'api',
'chief',
'dev',
'formal-member'
);
export const IsUnregistered = IsAccessIn('unregistered'); export const IsUnregistered = IsAccessIn('unregistered');
export const AccessMap: Record<WtsAccess, string> = { export const AccessMap: Record<WtsAccess, string> = {
'dev': '开发组','chief':'科长','api':'API','group-leader':'组长', dev: '开发组',
'formal-member':'正式成员','informal-member':'实习成员', chief: '科长',
'pre-member':'前成员','user':'用户', api: 'API',
'unregistered':'未注册用户', 'group-leader': '组长',
} 'formal-member': '正式成员',
'informal-member': '实习成员',
'pre-member': '前成员',
user: '用户',
unregistered: '未注册用户'
};
export type WtsISP = 'telecom' | 'unicom' | 'mobile' | 'broadnet' | 'others'; export type WtsISP = 'telecom' | 'unicom' | 'mobile' | 'broadnet' | 'others';
export const ISPMap: Record<WtsISP, string> = { export const ISPMap: Record<WtsISP, string> = {
'telecom': '电信', telecom: '电信',
'unicom': '联通', unicom: '联通',
'mobile': '移动', mobile: '移动',
'broadnet': '广电', broadnet: '广电',
'others': '其它', others: '其它'
} };
export type WtsStatus = 'fresh' | 'scheduled' | 'delay' | 'escalated' | 'solved' | 'canceled'; export type WtsStatus = 'fresh' | 'scheduled' | 'delay' | 'escalated' | 'solved' | 'canceled';
export const StatusMap: Record<WtsStatus, string> = { export const StatusMap: Record<WtsStatus, string> = {
'fresh': '待解决', fresh: '待解决',
'scheduled': '已预约', scheduled: '已预约',
'delay': '改日修', delay: '改日修',
'escalated': '已上报', escalated: '已上报',
'solved': '已解决', solved: '已解决',
'canceled': '已取消', canceled: '已取消'
} };
export type WtsPriority = 'highest' | 'assigned' | 'mainline' | 'normal' | 'in-passing' | 'least'; export type WtsPriority = 'highest' | 'assigned' | 'mainline' | 'normal' | 'in-passing' | 'least';
export const PriorityMap: Record<WtsPriority, string> = { export const PriorityMap: Record<WtsPriority, string> = {
'highest': '>>紧急派单!<<', highest: '>>紧急派单!<<',
'assigned': '运营商工单', assigned: '运营商工单',
'mainline': '主线任务', mainline: '主线任务',
'normal': '普通报修', normal: '普通报修',
'in-passing': '顺路看看', 'in-passing': '顺路看看',
'least': '最低', least: '最低'
} };
export type WtsCategory = export type WtsCategory =
| 'first-install'| 'low-speed'| 'ip-or-device'| 'client-or-account'| 'others'; | 'first-install'
| 'low-speed'
| 'ip-or-device'
| 'client-or-account'
| 'others';
export const CategoryMap: Record<WtsCategory, string> = { export const CategoryMap: Record<WtsCategory, string> = {
'first-install': '新装', 'first-install': '新装',
'low-speed': '网速慢', 'low-speed': '网速慢',
'ip-or-device': 'IP或设备问题', 'ip-or-device': 'IP或设备问题',
'client-or-account': '客户端或账号问题', 'client-or-account': '客户端或账号问题',
'others': '其它问题', others: '其它问题'
} };
export type WtsAPIErrorType = export type WtsAPIErrorType = 1 | 2 | 3 | 4 | 5;
| 1 | 2 | 3 | 4 | 5;
export const APIErrorTypeMap: Record<WtsAPIErrorType, string> = { export const APIErrorTypeMap: Record<WtsAPIErrorType, string> = {
1: '服务器内部错误,请联系我们的技术人员。', 1: '服务器内部错误,请联系我们的技术人员。',
2: '你的请求无效,可能是由于格式错误或不支持的操作。', 2: '你的请求无效,可能是由于格式错误或不支持的操作。',
3: '您没有进行该操作的权限。', 3: '您没有进行该操作的权限。',
4: '数据库出现错误。', 4: '数据库出现错误。',
5: '您的请求在逻辑上不被允许。', 5: '您的请求在逻辑上不被允许。'
} };
export type WtsZone = 'FX' | 'BM' | 'DM' | 'QT' | 'XHAB' | 'XHCD' | 'ZH' | 'other' | 'all'; export type WtsZone = 'FX' | 'BM' | 'DM' | 'QT' | 'XHAB' | 'XHCD' | 'ZH' | 'other' | 'all';
export const ZoneMap: Record<WtsZone, string> = { export const ZoneMap: Record<WtsZone, string> = {
'FX':'凤翔', FX: '凤翔',
'BM':'北门', BM: '北门',
'DM':'东门', DM: '东门',
'QT':'岐头', QT: '岐头',
'XHAB':'香晖AB', XHAB: '香晖AB',
'XHCD':'香晖CD', XHCD: '香晖CD',
'ZH':'朝晖', ZH: '朝晖',
'other':'其它', other: '其它',
'all':'全部', all: '全部'
} };
export const ZoneToBlock: Record<WtsZone, WtsBlock[]> = { export const ZoneToBlock: Record<WtsZone, WtsBlock[]> = {
'FX':['1','2','3','4','5','6'], FX: ['1', '2', '3', '4', '5', '6'],
'BM':['7','8','9','10','11'], BM: ['7', '8', '9', '10', '11'],
'DM':['12','13','14','15','20','21','22'], DM: ['12', '13', '14', '15', '20', '21', '22'],
'QT':['16','17','18','19'], QT: ['16', '17', '18', '19'],
'XHAB':['XHA','XHB'], XHAB: ['XHA', 'XHB'],
'XHCD':['XHC','XHD'], XHCD: ['XHC', 'XHD'],
'ZH':['ZH'], ZH: ['ZH'],
'other':['other'], other: ['other'],
'all':[ all: [
'1','2','3','4','5','6', '1',
'7','8','9','10','11', '2',
'12','13','14','15','20','21','22', '3',
'16','17','18','19', '4',
'XHA','XHB', '5',
'XHC','XHD', '6',
'7',
'8',
'9',
'10',
'11',
'12',
'13',
'14',
'15',
'20',
'21',
'22',
'16',
'17',
'18',
'19',
'XHA',
'XHB',
'XHC',
'XHD',
'ZH', 'ZH',
'other' 'other'
], ]
} };

View File

@@ -1,16 +1,16 @@
// 用于表单提交时的校验方便 // 用于表单提交时的校验方便
export class invalidState { export class invalidState {
notOK = $state(false); notOK = $state(false);
txt = $state(''); txt = $state('');
assert(x: boolean, e: string) { assert(x: boolean, e: string) {
if (!x && !this.notOK) { if (!x && !this.notOK) {
this.notOK = true; this.notOK = true;
this.txt = e; this.txt = e;
} }
} }
reset() { reset() {
this.notOK = false; this.notOK = false;
this.txt = ''; this.txt = '';
} }
} }

View File

@@ -1,7 +1,7 @@
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
export const load = (async () => { export const load = (async () => {
return {}; return {};
}) satisfies LayoutLoad; }) satisfies LayoutLoad;
export const prerender = true; export const prerender = true;

View File

@@ -1,9 +1,9 @@
<script> <script>
import RetroCard from '$lib/components/RetroCard.svelte'; import RetroCard from '$lib/components/RetroCard.svelte';
import { Button } from 'carbon-components-svelte'; import { Button } from 'carbon-components-svelte';
import group from '$lib/assets/picture/girl-using-computer.svg'; import group from '$lib/assets/picture/girl-using-computer.svg';
import help from '$lib/assets/picture/help.svg'; import help from '$lib/assets/picture/help.svg';
import outlook from '$lib/assets/picture/two-people-credit-card.svg'; import outlook from '$lib/assets/picture/two-people-credit-card.svg';
</script> </script>
<h1 style="font-size: 25px">欢迎使用ZSC网维报修系统</h1> <h1 style="font-size: 25px">欢迎使用ZSC网维报修系统</h1>
@@ -13,18 +13,18 @@
<br /> <br />
<RetroCard> <RetroCard>
<div style="display: flex; align-items: center; gap: 0.5rem;"> <div style="display: flex; align-items: center; gap: 0.5rem;">
<div> <div>
<h2>😵网络遇到了问题?</h2> <h2>😵网络遇到了问题?</h2>
<p>点击下方按钮,向我们提交报修。</p> <p>点击下方按钮,向我们提交报修。</p>
<Button href="/repair/new">我要报修!</Button> <Button href="/repair/new">我要报修!</Button>
</div> </div>
<img <img
src={help} src={help}
alt="Need help with network?" alt="Need help with network?"
style="width: 120px; height: 120px; object-fit: contain; margin-left: auto; align-self: flex-end;" style="width: 120px; height: 120px; object-fit: contain; margin-left: auto; align-self: flex-end;"
/> />
</div> </div>
</RetroCard> </RetroCard>
<br /> <br />
@@ -32,40 +32,43 @@
<br /> <br />
<RetroCard> <RetroCard>
<div style="display: flex; align-items: center; gap: 0.5rem;"> <div style="display: flex; align-items: center; gap: 0.5rem;">
<img <img
src={outlook} src={outlook}
alt="how to use campus network" alt="how to use campus network"
style="width: 120px; height: 120px; object-fit: contain; margin-right: 0.5rem;" style="width: 120px; height: 120px; object-fit: contain; margin-right: 0.5rem;"
/> />
<div style="display: flex; flex-direction: column; gap: 0.25rem;"> <div style="display: flex; flex-direction: column; gap: 0.25rem;">
<h2>🥺不会使用校园网?</h2> <h2>🥺不会使用校园网?</h2>
<p>点击下方按钮,我们准备了校园网方方面面的攻略!</p> <p>点击下方按钮,我们准备了校园网方方面面的攻略!</p>
<Button href="/help" kind="secondary" style="align-self: flex-end;transform: translate(-25px,0px);">网络攻略</Button> <Button
</div> href="/help"
</div> kind="secondary"
style="align-self: flex-end;transform: translate(-25px,0px);">网络攻略</Button
>
</div>
</div>
</RetroCard> </RetroCard>
<br /> <br />
<br /> <br />
<br /> <br />
<RetroCard> <RetroCard>
<div style="display: flex; align-items: center; gap: 0.5rem;"> <div style="display: flex; align-items: center; gap: 0.5rem;">
<div> <div>
<h2>🧑‍🤝‍🧑网络支持群</h2> <h2>🧑‍🤝‍🧑网络支持群</h2>
<p style="display: inline-block; margin: 0 0 0.5rem 0;"> <p style="display: inline-block; margin: 0 0 0.5rem 0;">
如果有其它问题您可以加入我们的网络支持QQ群我们的工作人员会详细解答任何问题。 如果有其它问题您可以加入我们的网络支持QQ群我们的工作人员会详细解答任何问题。
</p> </p>
<p style="display: inline-block; margin: 0;"> <p style="display: inline-block; margin: 0;">
群号:<strong>123123123</strong> 群号:<strong>123123123</strong>
</p> </p>
</div> </div>
<img <img
src={group} src={group}
alt="Join our support group" alt="Join our support group"
style="width: 120px; height: 120px; object-fit: contain;" style="width: 120px; height: 120px; object-fit: contain;"
/> />
</div> </div>
</RetroCard> </RetroCard>

View File

@@ -2,7 +2,10 @@
<br /> <br />
<hr /> <hr />
<br /> <br />
<p>中山学院网络维护科是一个独立的<strong>学生组织</strong>成立于2005年接受校信息中心的指导和管理。</p> <p>
中山学院网络维护科是一个独立的<strong>学生组织</strong
>成立于2005年接受校信息中心的指导和管理。
</p>
<p> <p>
网维的主要任务是保证校园宿舍网络的正常运行,处理解决在校学生所报修的有线校园网问题,这项业务主要通过依托于微信的网络报修系统来运行。 网维的主要任务是保证校园宿舍网络的正常运行,处理解决在校学生所报修的有线校园网问题,这项业务主要通过依托于微信的网络报修系统来运行。
</p> </p>
@@ -11,7 +14,8 @@
目前我们与三大运营商建立了良好的合作关系承接运营商在校园内的装维业务在信息中心的关怀下我们也承担学校部分IT后勤工作。 目前我们与三大运营商建立了良好的合作关系承接运营商在校园内的装维业务在信息中心的关怀下我们也承担学校部分IT后勤工作。
</p> </p>
<p> <p>
对于学生校园网报修,我们的成员会在<strong>每天16:30~18:00</strong>统一上门解决问题,用户在这个时间段必须本人在宿舍。 对于学生校园网报修,我们的成员会在<strong>每天16:30~18:00</strong
>统一上门解决问题,用户在这个时间段必须本人在宿舍。
</p> </p>
<p> <p>
网维一般会在每年的9~10月招新对象为当年新生要求有基础的计算机知识。具体招新事宜请以实际通告为准。 网维一般会在每年的9~10月招新对象为当年新生要求有基础的计算机知识。具体招新事宜请以实际通告为准。

View File

@@ -1,7 +1,7 @@
<script> <script>
import RetroCard from "$lib/components/RetroCard.svelte"; import RetroCard from '$lib/components/RetroCard.svelte';
</script> </script>
<h1>隐私权条款</h1> <h1>隐私权条款</h1>
<br /> <br />
<hr /> <hr />
@@ -9,56 +9,55 @@
<p> <p>
网维在向您提供服务时,需要和您的个人信息打交道。我们非常注重用户的数字个人隐私,对于有关问题,我们做出以下承诺: 网维在向您提供服务时,需要和您的个人信息打交道。我们非常注重用户的数字个人隐私,对于有关问题,我们做出以下承诺:
</p> </p>
<br/> <br />
<h2>我们需要您的什么信息?</h2> <h2>我们需要您的什么信息?</h2>
<p>为了派遣成员上门为您维修网络问题,在您注册报修系统时,我们需要获取您的:</p> <p>为了派遣成员上门为您维修网络问题,在您注册报修系统时,我们需要获取您的:</p>
<br/> <br />
<ul> <ul>
<li>宿舍住址</li> <li>宿舍住址</li>
<li>联系电话</li> <li>联系电话</li>
<li>校园卡账号</li> <li>校园卡账号</li>
</ul> </ul>
<br/> <br />
<p>此外,为了确认您身份的真实性,我们还需要您的学号和真实姓名。</p> <p>此外,为了确认您身份的真实性,我们还需要您的学号和真实姓名。</p>
<p> <p>
在初次注册时,您的微信账户与您提交的学号和真实姓名绑定,原则上无法改动,而其余个人信息皆可以随时修改。 在初次注册时,您的微信账户与您提交的学号和真实姓名绑定,原则上无法改动,而其余个人信息皆可以随时修改。
</p> </p>
<br/> <br />
<h2>我们如何保管您的信息?</h2> <h2>我们如何保管您的信息?</h2>
<p> <p>
我们使用妥善的措施,将您的个人信息保护在多项安全机制之内。除非根据下文所描述的信息使用条款合规将您的信息提供给有关人员之外,您提交给我们的个人信息绝不会被任何个人与集体知晓。 我们使用妥善的措施,将您的个人信息保护在多项安全机制之内。除非根据下文所描述的信息使用条款合规将您的信息提供给有关人员之外,您提交给我们的个人信息绝不会被任何个人与集体知晓。
</p> </p>
<br/> <br />
<p> <p>
当您修改您的个人信息,提交成功后,您的旧信息将在现有数据库中被永久删除。但是我们会定期备份我们的数据库,其中可能包含了旧的个人信息。不过,这些备份会被加密妥善保存,除非报修系统数据库遇到了损坏的情况,需要恢复备份的,否则任何人都无权访问这些数据。 当您修改您的个人信息,提交成功后,您的旧信息将在现有数据库中被永久删除。但是我们会定期备份我们的数据库,其中可能包含了旧的个人信息。不过,这些备份会被加密妥善保存,除非报修系统数据库遇到了损坏的情况,需要恢复备份的,否则任何人都无权访问这些数据。
</p> </p>
<br/> <br />
<h2>我们如何使用您的信息?</h2> <h2>我们如何使用您的信息?</h2>
<p> <p>
为了为您进行上门维修服务,当您在报修系统中有未解决的工单时,您的姓名,联系电话,宿舍住址和校园网账号将会在报修系统后台中被展示给有关的网维成员,以便它们进行上门维修或联系您进行其它相关事宜。当工单完成或被取消时,您的个人信息也将对普通网维成员不可见,但是您的信息会随着工单处理记录在管理层后台中保留一段时间,以供网维的管理层来进行审计工作。 为了为您进行上门维修服务,当您在报修系统中有未解决的工单时,您的姓名,联系电话,宿舍住址和校园网账号将会在报修系统后台中被展示给有关的网维成员,以便它们进行上门维修或联系您进行其它相关事宜。当工单完成或被取消时,您的个人信息也将对普通网维成员不可见,但是您的信息会随着工单处理记录在管理层后台中保留一段时间,以供网维的管理层来进行审计工作。
</p> </p>
<br/> <br />
<p> <p>
当遇到网维无法解决的问题,我们将启动上报程序,将您的网络问题告知运营商方面的有关工程师。此时,您的姓名,住址,学号等个人信息将会通过网维和运营商的对接机制,妥善地随着工单一并发送给您校园卡所在的运营商。 当遇到网维无法解决的问题,我们将启动上报程序,将您的网络问题告知运营商方面的有关工程师。此时,您的姓名,住址,学号等个人信息将会通过网维和运营商的对接机制,妥善地随着工单一并发送给您校园卡所在的运营商。
</p> </p>
<br/> <br />
<p> <p>
除此之外,<strong 除此之外,<strong
>我们不会将您的个人信息以任何方式泄露给任何个人或集体,尤其不会将您的信息用于商业营销或广告方面</strong >我们不会将您的个人信息以任何方式泄露给任何个人或集体,尤其不会将您的信息用于商业营销或广告方面</strong
> >
</p> </p>
<br/> <br />
<p> <p>
如果您发现有网维成员或者运营商方面有关人员使用您提供给网维的个人信息,进行除了网络维修及其有关事宜之外的活动的,您可以通过多种方式进行投诉: 如果您发现有网维成员或者运营商方面有关人员使用您提供给网维的个人信息,进行除了网络维修及其有关事宜之外的活动的,您可以通过多种方式进行投诉:
</p> </p>
<br/> <br />
<ul> <ul>
<li>在我们的报修系统后台聊天栏进行留言</li> <li>在我们的报修系统后台聊天栏进行留言</li>
<li>加入我们的网络支持QQ群私聊群管理反馈问题。</li> <li>加入我们的网络支持QQ群私聊群管理反馈问题。</li>
</ul> </ul>
<br/> <br />
<br /> <br />
<hr /> <hr />
<br /> <br />
<p>本条款最后更新于2025年12月27日最终解释权归网维所有。</p> <p>本条款最后更新于2025年12月27日最终解释权归网维所有。</p>

View File

@@ -14,4 +14,4 @@
<br /> <br />
<hr /> <hr />
<br /> <br />
<p>正在开发中</p> <p>正在开发中</p>

View File

@@ -16,9 +16,9 @@
Button, Button,
NotificationQueue, NotificationQueue,
Loading, Loading,
TextInput, TextInput,
Select, Select,
SelectItem SelectItem
} from 'carbon-components-svelte'; } from 'carbon-components-svelte';
import { IsRFC3339 } from '$lib/types/RFC3339'; import { IsRFC3339 } from '$lib/types/RFC3339';
import { invalidState } from '$lib/types/invalidState.svelte'; import { invalidState } from '$lib/types/invalidState.svelte';
@@ -30,8 +30,8 @@
let q: NotificationQueue; let q: NotificationQueue;
let r = $state({ let r = $state({
priority: 'normal', priority: 'normal'
} as NewTicketReq); } as NewTicketReq);
function onOccurDateChange(event: CustomEvent) { function onOccurDateChange(event: CustomEvent) {
const { dateStr } = event.detail; const { dateStr } = event.detail;
@@ -145,9 +145,9 @@
<p>为他人增添报修,注意,首先需要知道他人的学号。单独的工单增添正在开发中...</p> <p>为他人增添报修,注意,首先需要知道他人的学号。单独的工单增添正在开发中...</p>
<br /> <br />
<TextInput labelText="用户的学号" placeholder="请输入用户学号" bind:value={r.issuer_sid}/> <TextInput labelText="用户的学号" placeholder="请输入用户学号" bind:value={r.issuer_sid} />
<br/> <br />
<br/> <br />
<DatePicker datePickerType="single" on:change={onOccurDateChange}> <DatePicker datePickerType="single" on:change={onOccurDateChange}>
<DatePickerInput <DatePickerInput
@@ -205,22 +205,21 @@
<br /> <br />
<br /> <br />
<Select <Select
labelText="工单优先级" labelText="工单优先级"
bind:selected={r.priority} bind:selected={r.priority}
helperText="选择工单的优先级类型,更高优先级会在系统中优先显示" helperText="选择工单的优先级类型,更高优先级会在系统中优先显示"
> >
<SelectItem value="highest" text="十万火急" style="color: var(--color-red-600);" /> <SelectItem value="highest" text="十万火急" style="color: var(--color-red-600);" />
<SelectItem value="assigned" text="运营商工单" style="color: #2563eb;" /> <SelectItem value="assigned" text="运营商工单" style="color: #2563eb;" />
<SelectItem value="mainline" text="主线任务" /> <SelectItem value="mainline" text="主线任务" />
<SelectItem value="normal" text="一般报修" /> <SelectItem value="normal" text="一般报修" />
<SelectItem value="in-passing" text="顺路看看" /> <SelectItem value="in-passing" text="顺路看看" />
<SelectItem value="least" text="不紧急" /> <SelectItem value="least" text="不紧急" />
</Select> </Select>
<br/> <br />
<br/> <br />
<Button on:click={handleSubmit}>提交</Button> <Button on:click={handleSubmit}>提交</Button>
<NotificationQueue bind:this={q} /> <NotificationQueue bind:this={q} />
<Loading active={!notLoading} /> <Loading active={!notLoading} />

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
</script> </script>
<h1>管理排班</h1> <h1>管理排班</h1>
<br /> <br />
<hr /> <hr />
<br /> <br />
<p>正在开发中</p> <p>正在开发中</p>

View File

@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
</script> </script>
<h1>Forbidden</h1> <h1>Forbidden</h1>
<p>对不起,您没有权限访问这个页面。</p> <p>对不起,您没有权限访问这个页面。</p>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Accordion, AccordionItem } from 'carbon-components-svelte'; import { Accordion, AccordionItem } from 'carbon-components-svelte';
import RetroCard from '$lib/components/RetroCard.svelte'; import RetroCard from '$lib/components/RetroCard.svelte';
</script> </script>
<h1>校园网使用攻略</h1> <h1>校园网使用攻略</h1>
@@ -13,7 +13,7 @@
<br /> <br />
<RetroCard> <RetroCard>
<h3 style="font-size: 25px; ">⚙️ 常见问题</h3> <h3 style="font-size: 25px; ">⚙️ 常见问题</h3>
<br/> <br />
<Accordion size="xl"> <Accordion size="xl">
<AccordionItem title="电脑获取不到IP地址"> <AccordionItem title="电脑获取不到IP地址">
<p> <p>
@@ -54,7 +54,7 @@
<RetroCard> <RetroCard>
<h3 style="font-size: 25px; ">🤔 新生相关</h3> <h3 style="font-size: 25px; ">🤔 新生相关</h3>
<br/> <br />
<Accordion size="xl"> <Accordion size="xl">
<AccordionItem title="去哪里办校园网?"> <AccordionItem title="去哪里办校园网?">
<p> <p>
@@ -88,7 +88,7 @@
<br /> <br />
<RetroCard> <RetroCard>
<h3 style="font-size: 25px; ">🧐 关于报修</h3> <h3 style="font-size: 25px; ">🧐 关于报修</h3>
<br/> <br />
<Accordion size="xl"> <Accordion size="xl">
<AccordionItem title="如何报修我的网络故障?"> <AccordionItem title="如何报修我的网络故障?">
<p> <p>
@@ -125,4 +125,4 @@
</RetroCard> </RetroCard>
<br /> <br />
<br /> <br />

View File

@@ -3,44 +3,42 @@
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@plugin 'daisyui'; @plugin 'daisyui';
@plugin "daisyui/theme" { @plugin 'daisyui/theme' {
name: "wireframe"; name: 'wireframe';
default: true; default: true;
prefersdark: false; prefersdark: false;
color-scheme: "light"; color-scheme: 'light';
--color-base-100: #f4f4f4; --color-base-100: #f4f4f4;
--color-base-200: #e6e6e6; --color-base-200: #e6e6e6;
--color-base-300: #d5d5d5; --color-base-300: #d5d5d5;
--color-base-content: #000000; --color-base-content: #000000;
--color-primary: #0f62fe; --color-primary: #0f62fe;
--color-primary-content: oklch(100% 0 360); --color-primary-content: oklch(100% 0 360);
--color-secondary: #393939; --color-secondary: #393939;
--color-secondary-content: oklch(100% 0 360); --color-secondary-content: oklch(100% 0 360);
--color-accent: #0f62fe; --color-accent: #0f62fe;
--color-accent-content: oklch(100% 0 0); --color-accent-content: oklch(100% 0 0);
--color-neutral: #0f62fe; --color-neutral: #0f62fe;
--color-neutral-content: oklch(100% 0 0); --color-neutral-content: oklch(100% 0 0);
--color-info: #0f62fe; --color-info: #0f62fe;
--color-info-content: oklch(100% 0 360); --color-info-content: oklch(100% 0 360);
--color-success: #006d44; --color-success: #006d44;
--color-success-content: oklch(100% 0 360); --color-success-content: oklch(100% 0 360);
--color-warning: oklch(68% 0.162 75.834); --color-warning: oklch(68% 0.162 75.834);
--color-warning-content: #ffffff; --color-warning-content: #ffffff;
--color-error: #da1e28; --color-error: #da1e28;
--color-error-content: oklch(100% 0 360); --color-error-content: oklch(100% 0 360);
--radius-selector: 2rem; --radius-selector: 2rem;
--radius-field: 0rem; --radius-field: 0rem;
--radius-box: 0rem; --radius-box: 0rem;
--size-selector: 0.25rem; --size-selector: 0.25rem;
--size-field: 0.21875rem; --size-field: 0.21875rem;
--border: 0.5px; --border: 0.5px;
--depth: 1; --depth: 1;
--noise: 1; --noise: 1;
} }
@layer base { @layer base {
html { html {
/* 移动端优化:防止点击高亮 */ /* 移动端优化:防止点击高亮 */
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
@@ -55,5 +53,4 @@
/* 移动端优化:防止水平溢出 */ /* 移动端优化:防止水平溢出 */
@apply min-h-screen overflow-x-hidden bg-base-100 text-base-content; @apply min-h-screen overflow-x-hidden bg-base-100 text-base-content;
} }
}
}

View File

@@ -11,7 +11,7 @@
function gotoAuthAPI() { function gotoAuthAPI() {
if (!env.PUBLIC_JWT) { if (!env.PUBLIC_JWT) {
console.log("未找到PUBLIC_JWT") console.log('未找到PUBLIC_JWT');
} }
if (dev && env.PUBLIC_JWT) { if (dev && env.PUBLIC_JWT) {
docCookies.setItem('jwt', env.PUBLIC_JWT, Infinity, '/'); docCookies.setItem('jwt', env.PUBLIC_JWT, Infinity, '/');

View File

@@ -67,7 +67,12 @@
<RetroCard> <RetroCard>
<span style="display: flex; align-items: center;"> <span style="display: flex; align-items: center;">
<h2 style="margin-right: 0.5rem;">个人信息</h2> <h2 style="margin-right: 0.5rem;">个人信息</h2>
<Renew onclick={() => {TheLastPage.Write('/me'),goto('/login')}} style="cursor: pointer;" /> <Renew
onclick={() => {
(TheLastPage.Write('/me'), goto('/login'));
}}
style="cursor: pointer;"
/>
</span> </span>
<StructuredList style="margin-bottom: 1rem;"> <StructuredList style="margin-bottom: 1rem;">
<StructuredListBody> <StructuredListBody>

View File

@@ -77,7 +77,7 @@
scope: 'active', scope: 'active',
issuer: undefined, issuer: undefined,
block: ZoneToBlock[zone], block: ZoneToBlock[zone],
status: ['fresh','scheduled','escalated','delay'], status: ['fresh', 'scheduled', 'escalated', 'delay'],
priority: Object.keys(PriorityMap) as WtsPriority[], priority: Object.keys(PriorityMap) as WtsPriority[],
category: Object.keys(CategoryMap) as WtsCategory[], category: Object.keys(CategoryMap) as WtsCategory[],
isp: Object.keys(ISPMap) as WtsISP[], isp: Object.keys(ISPMap) as WtsISP[],
@@ -91,7 +91,7 @@
} as Criteria; } as Criteria;
} }
function jumpSearch(zone: WtsZone){ function jumpSearch(zone: WtsZone) {
Object.assign(criteria, search(zone)); Object.assign(criteria, search(zone));
goto('/op/tickets'); goto('/op/tickets');
} }
@@ -115,7 +115,10 @@
<div class="zone-tiles"> <div class="zone-tiles">
{#each zoneDisplayOrder as zone} {#each zoneDisplayOrder as zone}
{#if typeof countByZone?.[zone] !== 'undefined'} {#if typeof countByZone?.[zone] !== 'undefined'}
<Tile class={`zone-tile zone-${zoneTone(countByZone?.[zone])}`} on:click={() => jumpSearch(zone)}> <Tile
class={`zone-tile zone-${zoneTone(countByZone?.[zone])}`}
on:click={() => jumpSearch(zone)}
>
<span class="zone-name">{ZoneMap[zone]}</span> <span class="zone-name">{ZoneMap[zone]}</span>
<span class="zone-count">{countByZone?.[zone] ?? 0}</span> <span class="zone-count">{countByZone?.[zone] ?? 0}</span>
</Tile> </Tile>

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
</script> </script>
<h1>我的排班</h1> <h1>我的排班</h1>
<br/> <br />
<hr/> <hr />
<br/> <br />
<p>正在开发中</p> <p>正在开发中</p>

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type {PageProps} from './$types'; import type { PageProps } from './$types';
let {data}: PageProps = $props(); let { data }: PageProps = $props();
import {CheckAndGetJWT, Guard} from '$lib/jwt'; import { CheckAndGetJWT, Guard } from '$lib/jwt';
import {IsAdmin, IsOperator, PriorityMap, CategoryMap, ISPMap} from '$lib/types/enum'; import { IsAdmin, IsOperator, PriorityMap, CategoryMap, ISPMap } from '$lib/types/enum';
import {onMount} from 'svelte'; import { onMount } from 'svelte';
import { import {
RadioButtonGroup, RadioButtonGroup,
RadioButton, RadioButton,
@@ -22,20 +22,22 @@
NotificationQueue, NotificationQueue,
Toggle Toggle
} from 'carbon-components-svelte'; } from 'carbon-components-svelte';
import type {FilterTicketsReq} from '$lib/types/apiRequest'; import type { FilterTicketsReq } from '$lib/types/apiRequest';
import {ZoneMap, ZoneToBlock, StatusMap, type WtsZone} from '$lib/types/enum'; import { ZoneMap, ZoneToBlock, StatusMap, type WtsZone } from '$lib/types/enum';
import {RFC3339} from '$lib/types/RFC3339'; import { RFC3339 } from '$lib/types/RFC3339';
import {startOfDay, endOfDay} from 'date-fns'; import { startOfDay, endOfDay } from 'date-fns';
import {criteria} from '$lib/states/ticketCriteriaSearch.svelte'; import { criteria } from '$lib/states/ticketCriteriaSearch.svelte';
import {goto} from '$app/navigation'; import { goto } from '$app/navigation';
import type {WtsStatus, WtsPriority, WtsCategory, WtsISP} from '$lib/types/enum'; import type { WtsStatus, WtsPriority, WtsCategory, WtsISP } from '$lib/types/enum';
import {WtsJWT} from '$lib/jwt' import { WtsJWT } from '$lib/jwt';
onMount(() => Guard(IsOperator)); onMount(() => Guard(IsOperator));
let token: WtsJWT = $state({} as WtsJWT); let token: WtsJWT = $state({} as WtsJWT);
onMount(()=>{token =CheckAndGetJWT('parsed');}) onMount(() => {
token = CheckAndGetJWT('parsed');
});
let req = $state(criteria.r as FilterTicketsReq); let req = $state(criteria.r as FilterTicketsReq);
@@ -55,7 +57,7 @@
// }); // });
let onDateChange = (which: 'newer' | 'older') => (event: CustomEvent) => { let onDateChange = (which: 'newer' | 'older') => (event: CustomEvent) => {
const {dateStr} = event.detail; const { dateStr } = event.detail;
if (dateStr) { if (dateStr) {
const date = new Date(dateStr); const date = new Date(dateStr);
const adjustedDate = which === 'newer' ? startOfDay(date) : endOfDay(date); const adjustedDate = which === 'newer' ? startOfDay(date) : endOfDay(date);
@@ -85,8 +87,8 @@
const allStatuses = IsAdmin(token.access) const allStatuses = IsAdmin(token.access)
? (Object.keys(StatusMap) as WtsStatus[]) ? (Object.keys(StatusMap) as WtsStatus[])
: (Object.keys(StatusMap).filter( : (Object.keys(StatusMap).filter(
(status) => status !== 'solved' && status !== 'canceled' (status) => status !== 'solved' && status !== 'canceled'
) as WtsStatus[]); ) as WtsStatus[]);
const allPriorities = Object.keys(PriorityMap) as WtsPriority[]; const allPriorities = Object.keys(PriorityMap) as WtsPriority[];
const allCategories = Object.keys(CategoryMap) as WtsCategory[]; const allCategories = Object.keys(CategoryMap) as WtsCategory[];
const allISPs = Object.keys(ISPMap) as WtsISP[]; const allISPs = Object.keys(ISPMap) as WtsISP[];
@@ -116,7 +118,7 @@
'delay', 'delay',
'escalated' 'escalated'
] as const satisfies readonly WtsStatus[]; ] as const satisfies readonly WtsStatus[];
const statusOptions: readonly WtsStatus[] = statusOptionsAdmin //之前的区分没有意义,在后端会拦截的,在这里高花样,好像反而会破坏正常功能,感觉这个页面还是重写的样子。。 const statusOptions: readonly WtsStatus[] = statusOptionsAdmin; //之前的区分没有意义,在后端会拦截的,在这里高花样,好像反而会破坏正常功能,感觉这个页面还是重写的样子。。
const priorityOptions = [ const priorityOptions = [
'highest', 'highest',
@@ -184,7 +186,7 @@
$effect(() => { $effect(() => {
isScheduledSelected = req.status?.includes('scheduled') ?? false; isScheduledSelected = req.status?.includes('scheduled') ?? false;
}) });
</script> </script>
<h1>报修单检索</h1> <h1>报修单检索</h1>
@@ -194,11 +196,11 @@
<p>选择您需要检索报修工单的条件</p> <p>选择您需要检索报修工单的条件</p>
{#if IsAdmin(token.access)} {#if IsAdmin(token.access)}
<br /> <br />
<RadioButtonGroup id="scope" legendText="范围" bind:selected={req.scope} required={true}> <RadioButtonGroup id="scope" legendText="范围" bind:selected={req.scope} required={true}>
<RadioButton labelText="只看活跃的" value="active" /> <RadioButton labelText="只看活跃的" value="active" />
<RadioButton labelText="所有报修单" value="all" /> <RadioButton labelText="所有报修单" value="all" />
</RadioButtonGroup> </RadioButtonGroup>
{/if} {/if}
<br /> <br />
@@ -244,11 +246,14 @@
</Grid> </Grid>
</CheckboxGroup> </CheckboxGroup>
<div class="toggle"> <div class="toggle">
<Toggle size="sm" toggled={allSelected(zoneSelected, zoneOptions)} on:toggle={(e)=> { <Toggle
const { toggled } = e.detail as { toggled: boolean }; size="sm"
zoneSelected = toggled ? [...zoneOptions] : []; toggled={allSelected(zoneSelected, zoneOptions)}
on:toggle={(e) => {
const { toggled } = e.detail as { toggled: boolean };
zoneSelected = toggled ? [...zoneOptions] : [];
}} }}
> >
<span slot="labelA">全不选</span> <span slot="labelA">全不选</span>
<span slot="labelB">全选</span> <span slot="labelB">全选</span>
</Toggle> </Toggle>
@@ -272,22 +277,25 @@
<Checkbox value="escalated" labelText={StatusMap['escalated']} /> <Checkbox value="escalated" labelText={StatusMap['escalated']} />
</Column> </Column>
{#if IsAdmin(token.access)} {#if IsAdmin(token.access)}
<Column sm={2} md={2} lg={4}> <Column sm={2} md={2} lg={4}>
<Checkbox value="solved" labelText={StatusMap['solved']} /> <Checkbox value="solved" labelText={StatusMap['solved']} />
</Column> </Column>
<Column sm={2} md={2} lg={4}> <Column sm={2} md={2} lg={4}>
<Checkbox value="canceled" labelText={StatusMap['canceled']} /> <Checkbox value="canceled" labelText={StatusMap['canceled']} />
</Column> </Column>
{/if} {/if}
</Row> </Row>
</Grid> </Grid>
</CheckboxGroup> </CheckboxGroup>
<div class="toggle"> <div class="toggle">
<Toggle size="sm" toggled={allSelected(req.status, statusOptions)} on:toggle={(e)=> { <Toggle
const { toggled } = e.detail as { toggled: boolean }; size="sm"
req.status = toggled ? [...statusOptions] : []; toggled={allSelected(req.status, statusOptions)}
on:toggle={(e) => {
const { toggled } = e.detail as { toggled: boolean };
req.status = toggled ? [...statusOptions] : [];
}} }}
> >
<span slot="labelA">全不选</span> <span slot="labelA">全不选</span>
<span slot="labelB">全选</span> <span slot="labelB">全选</span>
</Toggle> </Toggle>
@@ -320,11 +328,14 @@
</Grid> </Grid>
</CheckboxGroup> </CheckboxGroup>
<div class="toggle"> <div class="toggle">
<Toggle size="sm" toggled={allSelected(req.priority, priorityOptions)} on:toggle={(e)=> { <Toggle
const { toggled } = e.detail as { toggled: boolean }; size="sm"
req.priority = toggled ? [...priorityOptions] : []; toggled={allSelected(req.priority, priorityOptions)}
on:toggle={(e) => {
const { toggled } = e.detail as { toggled: boolean };
req.priority = toggled ? [...priorityOptions] : [];
}} }}
> >
<span slot="labelA">全不选</span> <span slot="labelA">全不选</span>
<span slot="labelB">全选</span> <span slot="labelB">全选</span>
</Toggle> </Toggle>
@@ -353,11 +364,14 @@
</Grid> </Grid>
</CheckboxGroup> </CheckboxGroup>
<div class="toggle"> <div class="toggle">
<Toggle size="sm" toggled={allSelected(req.category, categoryOptions)} on:toggle={(e)=> { <Toggle
const { toggled } = e.detail as { toggled: boolean }; size="sm"
req.category = toggled ? [...categoryOptions] : []; toggled={allSelected(req.category, categoryOptions)}
on:toggle={(e) => {
const { toggled } = e.detail as { toggled: boolean };
req.category = toggled ? [...categoryOptions] : [];
}} }}
> >
<span slot="labelA">全不选</span> <span slot="labelA">全不选</span>
<span slot="labelB">全选</span> <span slot="labelB">全选</span>
</Toggle> </Toggle>
@@ -388,11 +402,14 @@
</Grid> </Grid>
</CheckboxGroup> </CheckboxGroup>
<div class="toggle"> <div class="toggle">
<Toggle size="sm" toggled={allSelected(req.isp, ispOptions)} on:toggle={(e)=> { <Toggle
const { toggled } = e.detail as { toggled: boolean }; size="sm"
req.isp = toggled ? [...ispOptions] : []; toggled={allSelected(req.isp, ispOptions)}
on:toggle={(e) => {
const { toggled } = e.detail as { toggled: boolean };
req.isp = toggled ? [...ispOptions] : [];
}} }}
> >
<span slot="labelA">全不选</span> <span slot="labelA">全不选</span>
<span slot="labelB">全选</span> <span slot="labelB">全选</span>
</Toggle> </Toggle>
@@ -410,12 +427,23 @@
<br /> <br />
<br /> <br />
<NumberInput min={1} max={20} step={1} bind:value={floor} allowEmpty={true} allowDecimal={false} <NumberInput
labelText="只看如下楼层(不填代表查看全部楼层)" /> min={1}
max={20}
step={1}
bind:value={floor}
allowEmpty={true}
allowDecimal={false}
labelText="只看如下楼层(不填代表查看全部楼层)"
/>
<br /> <br />
<br /> <br />
<Toggle labelText="只显示预约在今天的预约单" bind:toggled={viewTodayScheduled} disabled={!isScheduledSelected} /> <Toggle
labelText="只显示预约在今天的预约单"
bind:toggled={viewTodayScheduled}
disabled={!isScheduledSelected}
/>
<br /> <br />
<br /> <br />
<Button on:click={search}>搜索</Button> <Button on:click={search}>搜索</Button>

View File

@@ -40,8 +40,6 @@
const roomNum = Number.parseInt(digits, 10); const roomNum = Number.parseInt(digits, 10);
if (!Number.isFinite(roomNum)) return null; if (!Number.isFinite(roomNum)) return null;
const floor = Math.floor(roomNum / 100); const floor = Math.floor(roomNum / 100);
//console.log('getFloorFromRoom', { room, digits, roomNum, floor }); //console.log('getFloorFromRoom', { room, digits, roomNum, floor });
return Number.isFinite(floor) ? floor : null; return Number.isFinite(floor) ? floor : null;
@@ -60,11 +58,15 @@
if (criteria._floor !== null && criteria._floor !== undefined && criteria._floor !== 0) { if (criteria._floor !== null && criteria._floor !== undefined && criteria._floor !== 0) {
tickets = tickets.filter((t) => getFloorFromRoom(t?.issuer?.room) === criteria._floor); tickets = tickets.filter((t) => getFloorFromRoom(t?.issuer?.room) === criteria._floor);
} }
if (criteria._view_today_scheduled) { if (criteria._view_today_scheduled) {
const todayStart = new Date().setHours(0, 0, 0, 0); const todayStart = new Date().setHours(0, 0, 0, 0);
const todayEnd = new Date().setHours(23, 59, 59, 999); const todayEnd = new Date().setHours(23, 59, 59, 999);
tickets = tickets.filter((t) => t.status !== 'scheduled' || (toMs(t.appointed_at) >= todayStart && toMs(t.appointed_at) <= todayEnd)); tickets = tickets.filter(
} (t) =>
t.status !== 'scheduled' ||
(toMs(t.appointed_at) >= todayStart && toMs(t.appointed_at) <= todayEnd)
);
}
ok = true; ok = true;
ticketEmpty = tickets.length === 0; ticketEmpty = tickets.length === 0;
@@ -97,7 +99,7 @@
} }
} }
async function refreshTickets(){ async function refreshTickets() {
await fetchTickets1(); await fetchTickets1();
} }
</script> </script>
@@ -123,6 +125,11 @@
{:else} {:else}
<span>没有找到符合条件的报修单。</span> <span>没有找到符合条件的报修单。</span>
{/if} {/if}
<TicketDetail t={TicketModal.NowTicket} bind:open={TicketModal.Opened} src={TicketModal.SRC} onTicketChanged={refreshTickets}/> <TicketDetail
t={TicketModal.NowTicket}
bind:open={TicketModal.Opened}
src={TicketModal.SRC}
onTicketChanged={refreshTickets}
/>
<NotificationQueue bind:this={q} /> <NotificationQueue bind:this={q} />

View File

@@ -38,7 +38,7 @@
} }
} }
async function refreshTickets1(){ async function refreshTickets1() {
await fetchTickets(); await fetchTickets();
} }
</script> </script>
@@ -69,6 +69,11 @@
<UserTicket {t} /> <UserTicket {t} />
{/each} {/each}
<TicketDetail t={TicketModal.NowTicket} bind:open={TicketModal.Opened} src={TicketModal.SRC} onTicketChanged={refreshTickets1}/> <TicketDetail
t={TicketModal.NowTicket}
bind:open={TicketModal.Opened}
src={TicketModal.SRC}
onTicketChanged={refreshTickets1}
/>
<NotificationQueue bind:this={q} /> <NotificationQueue bind:this={q} />

View File

@@ -10,7 +10,7 @@
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"noImplicitAny": false,//暂时 "noImplicitAny": false, //暂时
"strictNullChecks": false "strictNullChecks": false
} }
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias