1.ticket-client初始化

2.导税平板修改
main
cysamurai 1 week ago
parent 5b3aa0d688
commit 5a11975bd5

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,22 @@
{
"sqltools.connections": [
{
"connectionTimeout": 15,
"mssqlOptions": {
"appName": "SQLTools",
"useUTC": true,
"encrypt": true,
"trustServerCertificate": false
},
"ssh": "Disabled",
"previewLimit": 50,
"server": "47.96.78.103",
"port": 1433,
"askForPassword": true,
"driver": "MSSQL",
"name": "QS aliyun",
"username": "sa",
"database": "QueuingSystem"
}
]
}

@ -0,0 +1,9 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
}
]
}

@ -0,0 +1,51 @@
// api/index.js
import request from '@/utils/request.js'
// 登录接口
export const userLogin = (loginData) => {
return request({
url: '/v1/auth/login',
method: 'POST',
data: loginData,
withToken: false // 登录请求本身通常不需要Token
})
}
// 今日进厅数据
export const getdailyEntry = (params) => {
return request({
url: '/v1/daily-entry/list',
method: 'get',
data: params,
withToken: true // 登录请求本身通常不需要Token
})
}
// 获取今日预约
export const getAppointmentToday = (params) => {
return request({
url: '/v1/appointment/today',
method: 'get',
data: params,
withToken: true // 登录请求本身通常不需要Token
})
}
// 票号管理
export const getTicket = (params) => {
return request({
url: '/v1/ticket/today_unified_tickets',
method: 'get',
data: params,
withToken: true // 登录请求本身通常不需要Token
})
}
// 获取业务列表
export const getBizList = () => {
return request({
url: '/v1/business',
method: 'get',
withToken: true // 登录请求本身通常不需要Token
})
}

@ -0,0 +1,119 @@
<!-- components/IDCardInput.vue -->
<template>
<view class="idcard-input-wrapper">
<input v-model="inputValue" class="idcard-input" placeholder="请输入18位身份证号码" type="text" maxlength="18"
@input="handleInput" @blur="handleBlur" />
<text v-if="showClear && inputValue" class="clear-btn" @click="clearInput">×</text>
</view>
</template>
<script setup>
import {
ref,
watch
} from 'vue';
const props = defineProps({
modelValue: String,
showClear: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:modelValue', 'change']);
const inputValue = ref(props.modelValue || '');
//
const handleInput = (e) => {
const value = e.detail.value.toUpperCase();
inputValue.value = value;
emit('update:modelValue', value);
//
const isValid = validateIDCard(value);
emit('change', value, isValid);
};
//
const handleBlur = () => {
const isValid = validateIDCard(inputValue.value);
emit('change', inputValue.value, isValid);
};
//
const clearInput = () => {
inputValue.value = '';
emit('update:modelValue', '');
emit('change', '', false);
};
//
const validateIDCard = (idCard) => {
if (!idCard) return false;
//
const reg = /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
if (!reg.test(idCard)) return false;
//
const idCardArray = idCard.split('');
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(idCardArray[i]) * factor[i];
}
const mod = sum % 11;
return parity[mod] === idCardArray[17].toUpperCase();
};
//
watch(() => props.modelValue, (newVal) => {
if (newVal !== inputValue.value) {
inputValue.value = newVal || '';
}
});
</script>
<style scoped>
.idcard-input-wrapper {
position: relative;
}
.idcard-input {
width: 100%;
height: 88rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 0 60rpx 0 24rpx;
font-size: 28rpx;
background: #fafafa;
box-sizing: border-box;
font-family: monospace;
}
.idcard-input:focus {
border-color: #007AFF;
background: white;
}
.clear-btn {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
font-size: 36rpx;
color: #999;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.1);
}
</style>

@ -0,0 +1,62 @@
// components/QueueTicketDetail/QueueTicketDetail.type.ts
// 票号状态枚举
export enum TicketStatus {
WAITING = 'waiting',
PROCESSING = 'processing',
COMPLETED = 'completed',
CANCELLED = 'cancelled'
}
// 评价结果枚举
export enum RatingResult {
EXCELLENT = 'excellent',
GOOD = 'good',
AVERAGE = 'average',
POOR = 'poor'
}
// 票号详情数据类型
export interface TicketDetail {
ticketNumber: string;
status: TicketStatus;
queueDate: string;
queueTime: string;
serviceWindow: string;
businessType: string;
staffName: string;
waitingDuration: string;
processingDuration: string;
rating?: RatingResult;
ratingPerson?: string;
ratingContact?: string;
}
// 组件Props类型定义
export interface QueueTicketDetailProps {
show: boolean;
ticketData?: TicketDetail;
title?: string;
}
// 组件Emits类型定义
export interface QueueTicketDetailEmits {
(e: 'update:show', value: boolean): void;
(e: 'close'): void;
}
// 默认票号数据导出
export const defaultTicketData: TicketDetail = {
ticketNumber: 'A001',
status: TicketStatus.COMPLETED,
queueDate: '2024-01-15',
queueTime: '09:30:25',
serviceWindow: '3号窗口',
businessType: '税务登记',
staffName: '张税务师',
waitingDuration: '15分钟',
processingDuration: '8分钟',
rating: RatingResult.EXCELLENT,
ratingPerson: '李先生',
ratingContact: '138****8888'
};

@ -0,0 +1,564 @@
<!-- components/QueueTicketDetail/QueueTicketDetail.vue -->
<template>
<!-- 弹窗遮罩 -->
<view v-if="show" class="ticket-modal-mask" @click="handleClose">
<view class="ticket-modal-content" @click.stop>
<!-- 弹窗头部 -->
<view class="ticket-modal-header">
<view class="header-main">
<text class="modal-title">{{ title }}</text>
<text class="modal-subtitle">排队取号详情信息</text>
</view>
<text class="close-btn" @click="handleClose">×</text>
</view>
<!-- 弹窗内容 -->
<scroll-view class="ticket-modal-body" scroll-y :show-scrollbar="true">
<!-- 票号基本信息卡片 -->
<view class="info-card">
<view class="card-header">
<text class="card-icon">🎫</text>
<text class="card-title">票号信息</text>
</view>
<view class="card-content">
<view class="info-row">
<text class="info-label">票号</text>
<text class="info-value highlight">{{ ticketData.ticketNumber }}</text>
</view>
<view class="info-row">
<text class="info-label">状态</text>
<view class="status-badge" :class="getStatusClass(ticketData.status)">
{{ getStatusText(ticketData.status) }}
</view>
</view>
</view>
</view>
<!-- 时间信息卡片 -->
<view class="info-card">
<view class="card-header">
<text class="card-icon"></text>
<text class="card-title">时间信息</text>
</view>
<view class="card-content compact">
<view class="time-grid">
<view class="time-item">
<text class="time-label">取号日期</text>
<text class="time-value">{{ ticketData.queueDate }}</text>
</view>
<view class="time-item">
<text class="time-label">取号时间</text>
<text class="time-value">{{ ticketData.queueTime }}</text>
</view>
<view class="time-item">
<text class="time-label">等候时长</text>
<text class="time-value duration">{{ ticketData.waitingDuration }}</text>
</view>
<view class="time-item">
<text class="time-label">办理时长</text>
<text class="time-value duration">{{ ticketData.processingDuration }}</text>
</view>
</view>
</view>
</view>
<!-- 办理信息卡片 -->
<view class="info-card">
<view class="card-header">
<text class="card-icon">🏢</text>
<text class="card-title">办理信息</text>
</view>
<view class="card-content compact">
<view class="info-row">
<text class="info-label">办理窗口</text>
<text class="info-value accent">{{ ticketData.serviceWindow }}</text>
</view>
<view class="info-row">
<text class="info-label">办理业务</text>
<text class="info-value">{{ ticketData.businessType }}</text>
</view>
<view class="info-row">
<text class="info-label">工作人员</text>
<text class="info-value primary">{{ ticketData.staffName }}</text>
</view>
</view>
</view>
<!-- 评价信息卡片 -->
<view v-if="ticketData.rating" class="info-card">
<view class="card-header">
<text class="card-icon"></text>
<text class="card-title">服务评价</text>
</view>
<view class="card-content">
<!-- 星级评价 -->
<view class="rating-display">
<view class="stars-container">
<text class="star" v-for="n in 5" :key="n"
:class="{ active: n <= getStarCount(ticketData.rating) }"></text>
</view>
<text class="rating-text">{{ getRatingText(ticketData.rating) }}</text>
</view>
<!-- 评价详情 -->
<view class="rating-details">
<view class="info-row">
<text class="info-label">评价人</text>
<text class="info-value">{{ ticketData.ratingPerson }}</text>
</view>
<view class="info-row">
<text class="info-label">联系方式</text>
<text class="info-value contact">{{ ticketData.ratingContact }}</text>
</view>
</view>
</view>
</view>
<!-- 无评价提示 -->
<view v-else class="empty-card">
<text class="empty-icon">💬</text>
<text class="empty-text">暂未收到评价</text>
<text class="empty-desc">服务完成后可进行评价</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type {
QueueTicketDetailProps,
QueueTicketDetailEmits,
TicketDetail
} from './QueueTicketDetail.type';
import {
defaultTicketData,
TicketStatus,
RatingResult
} from './QueueTicketDetail.type';
// Props
const props = withDefaults(defineProps<QueueTicketDetailProps>(), {
title: '票号详情',
ticketData: () => defaultTicketData
});
// Emits
const emit = defineEmits<QueueTicketDetailEmits>();
//
const ticketData = computed(() => props.ticketData);
//
const getStatusText = (status: TicketStatus): string => {
const statusMap = {
[TicketStatus.WAITING]: '等待中',
[TicketStatus.PROCESSING]: '办理中',
[TicketStatus.COMPLETED]: '已完成',
[TicketStatus.CANCELLED]: '已取消'
};
return statusMap[status] || '未知状态';
};
//
const getStatusClass = (status: TicketStatus): string => {
const classMap = {
[TicketStatus.WAITING]: 'status-waiting',
[TicketStatus.PROCESSING]: 'status-processing',
[TicketStatus.COMPLETED]: 'status-completed',
[TicketStatus.CANCELLED]: 'status-cancelled'
};
return classMap[status] || '';
};
//
const getRatingText = (rating: RatingResult): string => {
const ratingMap = {
[RatingResult.EXCELLENT]: '非常满意',
[RatingResult.GOOD]: '满意',
[RatingResult.AVERAGE]: '一般',
[RatingResult.POOR]: '不满意'
};
return ratingMap[rating] || '未评价';
};
//
const getStarCount = (rating: RatingResult): number => {
const starMap = {
[RatingResult.EXCELLENT]: 5,
[RatingResult.GOOD]: 4,
[RatingResult.AVERAGE]: 3,
[RatingResult.POOR]: 2
};
return starMap[rating] || 0;
};
//
const handleClose = () => {
emit('update:show', false);
emit('close');
};
</script>
<style scoped>
.ticket-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
padding: 40rpx;
}
.ticket-modal-content {
background: white;
border-radius: 20rpx;
width: 100%;
max-height: 75vh;
display: flex;
flex-direction: column;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.25);
overflow: hidden;
}
/* 头部样式 */
.ticket-modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 32rpx 32rpx 24rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: sticky;
top: 0;
z-index: 10;
}
.header-main {
flex: 1;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: white;
display: block;
margin-bottom: 8rpx;
line-height: 1.2;
}
.modal-subtitle {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
display: block;
line-height: 1.2;
}
.close-btn {
font-size: 40rpx;
color: white;
padding: 8rpx 16rpx;
font-weight: 300;
line-height: 1;
}
/* 主体内容 */
.ticket-modal-body {
flex: 1;
padding: 0 24rpx 24rpx;
max-height: calc(75vh - 120rpx);
}
/* 信息卡片 */
.info-card {
background: white;
border-radius: 16rpx;
margin: 24rpx 0 0;
border: 1rpx solid #e8e8e8;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.info-card:first-child {
margin-top: 24rpx;
}
.card-header {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 1rpx solid #f1f5f9;
}
.card-icon {
font-size: 28rpx;
margin-right: 16rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
}
.card-content {
padding: 28rpx;
}
.card-content.compact {
padding: 20rpx 28rpx;
}
/* 信息行 */
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
min-height: 40rpx;
}
.info-row:not(:last-child) {
border-bottom: 1rpx solid #f8fafc;
}
.info-label {
font-size: 28rpx;
color: #64748b;
font-weight: 500;
min-width: 180rpx;
}
.info-value {
font-size: 28rpx;
color: #1e293b;
font-weight: 600;
text-align: right;
flex: 1;
margin-left: 20rpx;
line-height: 1.4;
}
/* 特殊样式 */
.info-value.highlight {
color: #667eea;
font-size: 32rpx;
font-weight: bold;
}
.info-value.accent {
color: #f59e0b;
font-weight: bold;
}
.info-value.primary {
color: #10b981;
font-weight: bold;
}
.info-value.contact {
color: #667eea;
}
.info-value.duration {
color: #ef4444;
font-weight: bold;
}
/* 状态标签 */
.status-badge {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
min-width: 120rpx;
text-align: center;
}
.status-waiting {
background: #fffbeb;
color: #d97706;
border: 1rpx solid #fcd34d;
}
.status-processing {
background: #eff6ff;
color: #1d4ed8;
border: 1rpx solid #93c5fd;
}
.status-completed {
background: #f0fdf4;
color: #166534;
border: 1rpx solid #86efac;
}
.status-cancelled {
background: #fef2f2;
color: #dc2626;
border: 1rpx solid #fca5a5;
}
/* 时间网格 */
.time-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.time-item {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.time-label {
font-size: 24rpx;
color: #64748b;
font-weight: 500;
}
.time-value {
font-size: 26rpx;
color: #1e293b;
font-weight: 600;
}
/* 评价显示 */
.rating-display {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
background: #f8fafc;
border-radius: 12rpx;
margin-bottom: 24rpx;
}
.stars-container {
display: flex;
gap: 8rpx;
margin-bottom: 12rpx;
}
.star {
font-size: 36rpx;
color: #e2e8f0;
transition: color 0.3s ease;
}
.star.active {
color: #f59e0b;
}
.rating-text {
font-size: 26rpx;
font-weight: 600;
color: #1e293b;
}
.rating-details {
display: flex;
flex-direction: column;
gap: 16rpx;
}
/* 空状态 */
.empty-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 40rpx;
background: #f8fafc;
border-radius: 16rpx;
border: 2rpx dashed #e2e8f0;
margin-top: 24rpx;
}
.empty-icon {
font-size: 64rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #64748b;
font-weight: 600;
margin-bottom: 8rpx;
}
.empty-desc {
font-size: 24rpx;
color: #94a3b8;
}
/* 响应式设计 */
@media (min-width: 768px) {
.ticket-modal-content {
width: 85%;
max-width: 650px;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.ticket-modal-mask {
padding: 20rpx;
}
.ticket-modal-content {
max-height: 80vh;
}
.ticket-modal-body {
max-height: calc(80vh - 120rpx);
}
.info-row {
flex-direction: column;
align-items: flex-start;
gap: 8rpx;
padding: 16rpx 0;
}
.info-value {
text-align: left;
margin-left: 0;
}
.time-grid {
grid-template-columns: 1fr;
gap: 16rpx;
}
.status-badge {
align-self: flex-start;
}
}
/* 滚动条优化 */
::-webkit-scrollbar {
width: 4rpx;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2rpx;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2rpx;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>

@ -0,0 +1,4 @@
// components/QueueTicketDetail/index.ts
export { default as QueueTicketDetail } from './QueueTicketDetail.vue';
export type * from './QueueTicketDetail.type';
export { TicketStatus, RatingResult, defaultTicketData } from './QueueTicketDetail.type';

@ -0,0 +1,123 @@
// components/TaxHealthReport/TaxHealthReport.type.ts
// 报告数据结构类型定义
export interface TaxRegistrationData {
basicInfoConsistency: string;
taxTypeMatch: string;
}
export interface VatDeclarationData {
incomeConsistency: string;
taxBurdenRate: string;
}
export interface CorporateTaxData {
costRate: string;
lossContinuity: string;
}
export interface DeclarationData {
vat: VatDeclarationData;
corporateTax: CorporateTaxData;
}
export interface UpstreamInvoiceData {
supplierStatus: string;
businessConsistency: string;
}
export interface DownstreamInvoiceData {
issuingPattern: string;
inputOutputMatch: string;
}
export interface InvoiceData {
upstream: UpstreamInvoiceData;
downstream: DownstreamInvoiceData;
}
export interface SpecialRisksData {
relatedPartyPricing: string;
taxIncentives: string;
assetDisposal: string;
}
export interface ReportData {
registration: TaxRegistrationData;
declaration: DeclarationData;
invoice: InvoiceData;
specialRisks: SpecialRisksData;
materials: string[];
}
// 组件Props类型定义
export interface TaxHealthReportProps {
show: boolean;
reportData?: ReportData;
title?: string;
}
// 组件Emits类型定义
export interface TaxHealthReportEmits {
(e: 'update:show', value: boolean): void;
(e: 'close'): void;
(e: 'confirm'): void;
}
// 企业基本信息类型
export interface CompanyInfo {
name: string;
legalRepresentative: string;
registeredCapital: string;
establishmentDate: string;
creditCode: string;
phone: string;
email: string;
address: string;
}
// 组件Props类型定义更新
export interface TaxHealthReportProps {
show: boolean;
title?: string;
companyInfo?: CompanyInfo; // 新增企业信息参数
}
// 默认报告数据导出
export const defaultReportData: ReportData = {
registration: {
basicInfoConsistency: '税务登记信息与营业执照一致',
taxTypeMatch: '税种认定与业务基本匹配,建议复核小规模纳税人认定'
},
declaration: {
vat: {
incomeConsistency: '增值税与企业所得税申报收入差异在合理范围内',
taxBurdenRate: '近3个月税负率3.2%,与行业均值基本持平'
},
corporateTax: {
costRate: '成本费用率68%,处于制造业合理区间',
lossContinuity: '近3年无连续亏损情况'
}
},
invoice: {
upstream: {
supplierStatus: '主要供应商状态正常,未发现异常户',
businessConsistency: '发票内容与实际业务基本一致'
},
downstream: {
issuingPattern: '未发现大额整数开票等异常模式',
inputOutputMatch: '进销项匹配度良好,个别月份需关注'
}
},
specialRisks: {
relatedPartyPricing: '关联交易价格符合独立交易原则',
taxIncentives: '高新技术企业资质维护良好,研发费用占比达标',
assetDisposal: '固定资产处置程序合规,相关备案完整'
},
materials: [
'近三年审计报告及财务报表',
'各税种纳税申报表及完税凭证',
'进项发票与销项发票明细数据',
'税收优惠资格认定文件'
]
};

@ -0,0 +1,814 @@
<!-- components/TaxHealthReport/TaxHealthReport.vue -->
<template>
<!-- 弹窗遮罩 -->
<view v-if="show" class="tax-report-modal-mask" @click="handleClose">
<view class="tax-report-modal-content" @click.stop>
<!-- 报告头部 -->
<view class="tax-report-header">
<view class="header-left">
<text class="tax-report-title">{{ title }}</text>
<text class="tax-report-subtitle">企业税务健康度分析报告</text>
</view>
<text class="tax-report-close-btn" @click="handleClose">×</text>
</view>
<!-- 报告内容 -->
<scroll-view class="tax-report-body" scroll-y :show-scrollbar="false">
<!-- 企业基本信息 -->
<view class="company-info-section">
<view class="section-header">
<view class="section-title-wrapper">
<text class="section-icon">🏢</text>
<text class="section-title">企业基本信息</text>
</view>
</view>
<view class="company-info-grid">
<view class="info-row">
<view class="info-item">
<text class="info-label">企业名称</text>
<text class="info-value">{{ companyInfo.name }}</text>
</view>
<view class="info-item">
<text class="info-label">法定代表人</text>
<text class="info-value">{{ companyInfo.legalRepresentative }}</text>
</view>
</view>
<view class="info-row">
<view class="info-item">
<text class="info-label">注册资本</text>
<text class="info-value">{{ companyInfo.registeredCapital }}</text>
</view>
<view class="info-item">
<text class="info-label">成立日期</text>
<text class="info-value">{{ companyInfo.establishmentDate }}</text>
</view>
</view>
<view class="info-row">
<view class="info-item full-width">
<text class="info-label">统一社会信用代码</text>
<text class="info-value credit-code">{{ companyInfo.creditCode }}</text>
</view>
</view>
<view class="info-row">
<view class="info-item">
<text class="info-label">联系电话</text>
<text class="info-value contact-info">{{ companyInfo.phone }}</text>
</view>
<view class="info-item">
<text class="info-label">邮箱地址</text>
<text class="info-value contact-info">{{ companyInfo.email }}</text>
</view>
</view>
<view class="info-row">
<view class="info-item full-width">
<text class="info-label">注册地址</text>
<text class="info-value address">{{ companyInfo.address }}</text>
</view>
</view>
</view>
</view>
<!-- 健康度概览 -->
<view class="health-overview">
<view class="health-score">
<view class="score-circle">
<text class="score-value">85</text>
<text class="score-label">健康度</text>
</view>
</view>
<view class="health-stats">
<view class="stat-item">
<text class="stat-value">4</text>
<text class="stat-label">检查项目</text>
</view>
<view class="stat-item">
<text class="stat-value">3</text>
<text class="stat-label">低风险</text>
</view>
<view class="stat-item">
<text class="stat-value">1</text>
<text class="stat-label">需关注</text>
</view>
</view>
</view>
<!-- 税务登记与信息核对 -->
<view class="report-section">
<view class="section-header">
<view class="section-title-wrapper">
<text class="section-number">01</text>
<text class="section-title">税务登记与信息核对</text>
</view>
<view class="risk-tag low-risk">低风险</view>
</view>
<view class="section-content">
<view class="chart-container">
<view class="info-match-chart">
<view class="chart-bar">
<view class="bar-fill" :style="{width: '90%'}"></view>
</view>
<text class="chart-label">信息匹配度 90%</text>
</view>
</view>
<view class="detail-list">
<view class="detail-item">
<view class="item-header">
<text class="item-icon">📋</text>
<text class="item-title">税务登记信息</text>
</view>
<text class="item-desc">企业名称地址法人等信息与营业执照一致</text>
</view>
<view class="detail-item">
<view class="item-header">
<text class="item-icon">🔍</text>
<text class="item-title">税种认定匹配</text>
</view>
<text class="item-desc">增值税纳税人类型与业务规模匹配建议复核小规模纳税人认定</text>
</view>
</view>
</view>
</view>
<!-- 申报数据逻辑 -->
<view class="report-section">
<view class="section-header">
<view class="section-title-wrapper">
<text class="section-number">02</text>
<text class="section-title">申报数据逻辑</text>
</view>
<view class="risk-tag medium-risk">需关注</view>
</view>
<view class="section-content">
<view class="declaration-charts">
<view class="chart-item">
<view class="chart-title">增值税申报</view>
<view class="progress-chart">
<view class="progress-bar">
<view class="progress-fill" :style="{width: '85%'}"></view>
</view>
<text class="progress-text">收入一致性 85%</text>
</view>
<view class="chart-data">
<text class="data-item"> 税负率: 3.2% (行业平均: 3.5%)</text>
<text class="data-item"> 申报差异: 在合理范围内</text>
</view>
</view>
<view class="chart-item">
<view class="chart-title">企业所得税申报</view>
<view class="progress-chart">
<view class="progress-bar">
<view class="progress-fill" :style="{width: '78%'}"></view>
</view>
<text class="progress-text">成本合理性 78%</text>
</view>
<view class="chart-data">
<text class="data-item"> 成本费用率: 68%</text>
<text class="data-item"> 制造业合理区间: 60%-80%</text>
</view>
</view>
</view>
</view>
</view>
<!-- 发票管理风险 -->
<view class="report-section">
<view class="section-header">
<view class="section-title-wrapper">
<text class="section-number">03</text>
<text class="section-title">发票管理风险</text>
</view>
<view class="risk-tag low-risk">低风险</view>
</view>
<view class="section-content">
<view class="risk-distribution">
<view class="risk-item upstream-risk">
<view class="risk-icon">📥</view>
<text class="risk-type">上游发票合规性</text>
<view class="risk-status safe">正常</view>
<text class="risk-desc">供应商状态正常业务一致</text>
</view>
<view class="risk-item downstream-risk">
<view class="risk-icon">📤</view>
<text class="risk-type">下游开票合规性</text>
<view class="risk-status safe">正常</view>
<text class="risk-desc">开票模式合理进销匹配</text>
</view>
</view>
</view>
</view>
<!-- 特定业务风险点 -->
<view class="report-section">
<view class="section-header">
<view class="section-title-wrapper">
<text class="section-number">04</text>
<text class="section-title">特定业务风险点</text>
</view>
<view class="risk-tag low-risk">低风险</view>
</view>
<view class="section-content">
<view class="risk-points">
<view class="risk-point">
<view class="point-header">
<text class="point-icon"></text>
<text class="point-title">关联交易定价</text>
</view>
<text class="point-status compliant">符合独立交易原则</text>
</view>
<view class="risk-point">
<view class="point-header">
<text class="point-icon">🎯</text>
<text class="point-title">税收优惠适用</text>
</view>
<text class="point-status compliant">高新技术企业资质有效</text>
</view>
<view class="risk-point">
<view class="point-header">
<text class="point-icon">🏢</text>
<text class="point-title">资产处理合规性</text>
</view>
<text class="point-status compliant">固定资产处置程序完整</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import type { TaxHealthReportProps, TaxHealthReportEmits } from './TaxHealthReport.type';
// Props
defineProps<TaxHealthReportProps>();
// Emits
const emit = defineEmits<TaxHealthReportEmits>();
//
const companyInfo = reactive({
name: '浙江某某科技有限公司',
legalRepresentative: '张三',
registeredCapital: '1000万元人民币',
establishmentDate: '2018-06-15',
creditCode: '91330100MA2A2B2C2D',
phone: '0571-88888888',
email: 'contact@example.com',
address: '浙江省杭州市西湖区文三路XXX号创业大厦A座1001室'
});
//
const handleClose = () => {
emit('update:show', false);
emit('close');
};
</script>
<style scoped>
.tax-report-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
padding: 40rpx;
}
.tax-report-modal-content {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-radius: 24rpx;
width: 100%;
height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.2);
border: 1rpx solid #e2e8f0;
}
/* 头部样式 */
.tax-report-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 40rpx 32rpx 24rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx 24rpx 0 0;
position: sticky;
top: 0;
z-index: 10;
}
.header-left {
flex: 1;
}
.tax-report-title {
font-size: 36rpx;
font-weight: bold;
color: white;
display: block;
margin-bottom: 8rpx;
}
.tax-report-subtitle {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
display: block;
}
.tax-report-close-btn {
font-size: 44rpx;
color: white;
padding: 16rpx;
font-weight: 300;
margin-top: -8rpx;
}
/* 主体内容 */
.tax-report-body {
flex: 1;
padding: 32rpx;
max-height: calc(90vh - 120rpx);
}
/* 企业基本信息 */
.company-info-section {
background: white;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f1f5f9;
border-left: 6rpx solid #667eea;
}
.company-info-section .section-header {
margin-bottom: 24rpx;
}
.section-icon {
font-size: 28rpx;
margin-right: 12rpx;
}
.company-info-grid {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.info-row {
display: flex;
gap: 20rpx;
}
.info-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item.full-width {
flex: 2;
}
.info-label {
font-size: 24rpx;
color: #64748b;
font-weight: 500;
}
.info-value {
font-size: 26rpx;
color: #1e293b;
font-weight: 600;
word-break: break-all;
}
.info-value.credit-code {
font-family: 'Courier New', monospace;
background: #f8fafc;
padding: 12rpx 16rpx;
border-radius: 8rpx;
border: 1rpx solid #e2e8f0;
font-size: 24rpx;
}
.info-value.contact-info {
color: #667eea;
}
.info-value.address {
line-height: 1.5;
background: #f8fafc;
padding: 16rpx;
border-radius: 8rpx;
border: 1rpx solid #e2e8f0;
}
/* 健康度概览 */
.health-overview {
display: flex;
align-items: center;
background: white;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f1f5f9;
}
.health-score {
flex: 1;
display: flex;
justify-content: center;
}
.score-circle {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
background: conic-gradient(#10b981 85%, #e2e8f0 0%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
.score-circle::before {
content: '';
position: absolute;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: white;
}
.score-value {
font-size: 32rpx;
font-weight: bold;
color: #10b981;
z-index: 1;
}
.score-label {
font-size: 22rpx;
color: #64748b;
z-index: 1;
margin-top: 4rpx;
}
.health-stats {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.stat-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.stat-value {
font-size: 28rpx;
font-weight: bold;
color: #1e293b;
min-width: 40rpx;
}
.stat-label {
font-size: 24rpx;
color: #64748b;
}
/* 报告区块 */
.report-section {
background: white;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f1f5f9;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-title-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
}
.section-number {
font-size: 24rpx;
font-weight: bold;
color: #667eea;
background: #f0f4ff;
padding: 8rpx 16rpx;
border-radius: 12rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
}
/* 风险标签 */
.risk-tag {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
}
.low-risk {
background: #d1fae5;
color: #065f46;
}
.medium-risk {
background: #fef3c7;
color: #92400e;
}
/* 图表容器 */
.chart-container {
margin-bottom: 24rpx;
}
.info-match-chart {
text-align: center;
}
.chart-bar {
width: 100%;
height: 24rpx;
background: #e2e8f0;
border-radius: 12rpx;
margin-bottom: 16rpx;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, #10b981, #34d399);
border-radius: 12rpx;
transition: width 0.8s ease;
}
.chart-label {
font-size: 24rpx;
color: #64748b;
}
/* 详情列表 */
.detail-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-item {
background: #f8fafc;
border-radius: 16rpx;
padding: 24rpx;
border-left: 4rpx solid #667eea;
}
.item-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.item-icon {
font-size: 24rpx;
}
.item-title {
font-size: 26rpx;
font-weight: 600;
color: #1e293b;
}
.item-desc {
font-size: 24rpx;
color: #64748b;
line-height: 1.5;
}
/* 申报图表 */
.declaration-charts {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.chart-item {
background: #f8fafc;
border-radius: 16rpx;
padding: 24rpx;
}
.chart-title {
font-size: 26rpx;
font-weight: 600;
color: #1e293b;
margin-bottom: 20rpx;
}
.progress-chart {
margin-bottom: 16rpx;
}
.progress-bar {
width: 100%;
height: 16rpx;
background: #e2e8f0;
border-radius: 8rpx;
margin-bottom: 12rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 8rpx;
transition: width 0.8s ease;
}
.progress-text {
font-size: 22rpx;
color: #64748b;
}
.chart-data {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.data-item {
font-size: 22rpx;
color: #64748b;
}
/* 风险分布 */
.risk-distribution {
display: flex;
gap: 20rpx;
}
.risk-item {
flex: 1;
background: #f8fafc;
border-radius: 16rpx;
padding: 24rpx;
text-align: center;
border: 2rpx solid #e2e8f0;
}
.risk-icon {
font-size: 32rpx;
margin-bottom: 12rpx;
}
.risk-type {
font-size: 24rpx;
font-weight: 600;
color: #1e293b;
display: block;
margin-bottom: 8rpx;
}
.risk-status {
padding: 6rpx 16rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 500;
margin-bottom: 8rpx;
}
.safe {
background: #d1fae5;
color: #065f46;
}
.risk-desc {
font-size: 22rpx;
color: #64748b;
}
/* 风险点 */
.risk-points {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.risk-point {
display: flex;
justify-content: space-between;
align-items: center;
background: #f8fafc;
border-radius: 16rpx;
padding: 20rpx 24rpx;
}
.point-header {
display: flex;
align-items: center;
gap: 12rpx;
}
.point-icon {
font-size: 24rpx;
}
.point-title {
font-size: 24rpx;
font-weight: 600;
color: #1e293b;
}
.point-status {
padding: 6rpx 16rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 500;
}
.compliant {
background: #d1fae5;
color: #065f46;
}
/* 响应式设计 */
@media (min-width: 768px) {
.tax-report-modal-content {
width: 80%;
max-width: 800px;
}
.risk-distribution {
gap: 32rpx;
}
.declaration-charts {
flex-direction: row;
}
.chart-item {
flex: 1;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.info-row {
flex-direction: column;
gap: 16rpx;
}
.info-item {
width: 100%;
}
.risk-distribution {
flex-direction: column;
}
}
</style>

@ -0,0 +1,35 @@
<!-- components/TaxHealthReport/TaxReportItem.vue -->
<template>
<view class="tax-report-item">
<text class="tax-report-item-label">{{ label }}</text>
<text class="tax-report-item-value">{{ value }}</text>
</view>
</template>
<script setup lang="ts">
defineProps<{
label: string;
value: string;
}>();
</script>
<style scoped>
.tax-report-item {
flex-direction: row;
margin-bottom: 12rpx;
align-items: flex-start;
}
.tax-report-item-label {
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
.tax-report-item-value {
font-size: 26rpx;
color: #333;
line-height: 1.5;
flex: 1;
}
</style>

@ -0,0 +1,37 @@
<!-- components/TaxHealthReport/TaxReportSection.vue -->
<template>
<view class="tax-report-section">
<text class="tax-report-section-title">{{ title }}</text>
<view class="tax-report-section-content">
<slot />
</view>
</view>
</template>
<script setup lang="ts">
defineProps<{
title: string;
}>();
</script>
<style scoped>
.tax-report-section {
margin-bottom: 40rpx;
background: #f8f9fa;
border-radius: 15rpx;
padding: 25rpx;
border-left: 6rpx solid #007AFF;
}
.tax-report-section-title {
font-size: 32rpx;
font-weight: bold;
color: #007AFF;
margin-bottom: 20rpx;
display: block;
}
.tax-report-section-content {
margin-left: 10rpx;
}
</style>

@ -0,0 +1,37 @@
<!-- components/TaxHealthReport/TaxReportSubsection.vue -->
<template>
<view class="tax-report-subsection">
<text class="tax-report-subsection-title">{{ title }}</text>
<view class="tax-report-subsection-content">
<slot />
</view>
</view>
</template>
<script setup lang="ts">
defineProps<{
title: string;
}>();
</script>
<style scoped>
.tax-report-subsection {
margin-bottom: 25rpx;
background: white;
border-radius: 10rpx;
padding: 20rpx;
border: 1rpx solid #e9ecef;
}
.tax-report-subsection-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 15rpx;
display: block;
}
.tax-report-subsection-content {
margin-left: 15rpx;
}
</style>

@ -0,0 +1,4 @@
// components/TaxHealthReport/index.ts
export { default as TaxHealthReport } from './TaxHealthReport.vue';
export type * from './TaxHealthReport.type';
export { defaultReportData } from './TaxHealthReport.type';

@ -0,0 +1,20 @@
{
"string": [
{
"name": "app_name",
"value": "应用名称"
},
{
"name": "agcit_agc_common_uniapp",
"value": "1.0.0"
},
{
"name": "agcittype_agc_common_uniapp",
"value": "industryTemplate"
},
{
"name": "agcittype_agc_common_uniapp_plugin",
"value": "AI Plugin"
}
]
}

@ -44,21 +44,21 @@
</div> </div>
</div> </div>
<div class="row2-item2"> <div class="row2-item2">
<!-- <div class="row2-item2-left col-center"> <div class="row2-item2-left col-center">
<uni-icons type="home-filled" size="60" color="#fff"></uni-icons> <uni-icons type="home-filled" size="60" color="#fff"></uni-icons>
<p class="btn-p">快捷办税</p> <p class="btn-p">预留功能</p>
</div> </div>
<div style="width: 2vh;"></div> <div style="width: 2vh;"></div>
<div class="row2-item2-center col-center" @click="goToMultiInOne()"> <div class="row2-item2-center col-center" @click="goToMultiInOne()">
<uni-icons type="auth-filled" size="60" color="#fff"></uni-icons> <uni-icons type="auth-filled" size="60" color="#fff"></uni-icons>
<p class="btn-p">一户式</p> <p class="btn-p">预留功能</p>
</div> </div>
<div style="width: 2vh;"></div> <div style="width: 2vh;"></div>
<div class="row2-item2-right col-center" > <div class="row2-item2-right col-center" >
<uni-icons type="settings-filled" size="60" color="#fff"></uni-icons> <uni-icons type="settings-filled" size="60" color="#fff"></uni-icons>
<p class="btn-p">网格员</p> <p class="btn-p">预留功能</p>
</div> --> </div>
<div v-for="item in 3" style="flex: 1;"></div> <!-- <div v-for="item in 3" style="flex: 1;"></div> -->
</div> </div>
</div> </div>
</div> </div>

@ -0,0 +1,638 @@
<template>
<view class="page-container">
<view class="content">
<!-- 查询区域 -->
<view class="search-section">
<view class="search-card">
<tn-input v-model="searchValue" :placeholder="!isFromTicket ? '请输入企业名称或税号':'请输入票号号码或验证码'" border height="80rpx">
<template #prefix>
<uni-icons type="search" size="20" color="#999"></uni-icons>
</template>
</tn-input>
<tn-button type="primary" @click="handleSearch" :loading="loading" class="search-btn">
查询
</tn-button>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</view>
</view>
<!-- 本月未办事项 -->
<view v-if="pendingItems.length > 0 && isFromTicket" class="pending-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">本月未办事项</text>
<text class="item-count">{{ pendingItems.length }}</text>
</view>
<view class="pending-list">
<view v-for="(item, index) in pendingItems" :key="index" class="pending-item">
<view class="item-main">
<text class="item-name">{{ item.name }}</text>
<text class="item-deadline">截止日期{{ item.deadline }}</text>
</view>
<view class="item-actions">
<tn-button size="sm" type="primary" plain @click="handleGuide(item)">
引导办理
</tn-button>
</view>
</view>
</view>
</view>
</view>
<!-- 企业基本信息 -->
<view v-if="enterpriseInfo" class="info-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">企业基本信息</text>
</view>
<view class="enterprise-info">
<view class="info-row">
<text class="info-label">企业名称</text>
<text class="info-value">{{ enterpriseInfo.name }}</text>
</view>
<view class="info-row">
<text class="info-label">纳税人识别号</text>
<text class="info-value">{{ enterpriseInfo.taxId }}</text>
</view>
<view class="info-row">
<text class="info-label">企业类型</text>
<text class="info-value">{{ enterpriseInfo.type }}</text>
</view>
<view class="info-row">
<text class="info-label">注册地址</text>
<text class="info-value">{{ enterpriseInfo.address }}</text>
</view>
<view class="info-row">
<text class="info-label">法定代表人</text>
<text class="info-value">{{ enterpriseInfo.legalPerson }}</text>
</view>
</view>
</view>
</view>
<!-- 税务健康度图表 -->
<view v-if="showCharts" class="chart-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">税务健康度分析</text>
</view>
<view class="charts-container">
<!-- 健康度评分 -->
<view class="chart-item">
<view class="chart-title">综合健康评分</view>
<view class="score-ring">
<text class="score-text">{{ taxHealth.score }}</text>
<text class="score-label"></text>
</view>
<view class="score-level" :class="getScoreClass(taxHealth.score)">
{{ taxHealth.level }}
</view>
</view>
<!-- 指标雷达图 -->
<view class="chart-item">
<view class="chart-title">各项指标</view>
<!-- <canvas canvas-id="radarChart" class="chart-canvas"></canvas> -->
<qiun-data-charts type="radar" :chartData="chartData" :canvas2d="true" canvasId="radarChart"
class="chart-canvas" :opts="opts" />
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!enterpriseInfo && searched" class="empty-section">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">未找到相关企业信息</text>
</view>
</view>
<!-- 办理方式选择弹窗 -->
<uni-popup ref="actionPopup" type="center" background-color="#fff" class="action-popup">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">选择办理方式</text>
<uni-icons type="close" size="20" color="#999" @click="closePopup"></uni-icons>
</view>
<view class="popup-body">
<text class="current-task">当前事项{{ currentTask?.name }}</text>
<view class="action-options">
<view class="action-item" @click="handlePushMessage">
<view class="action-icon">
<uni-icons type="notification" size="30" color="#0099ff"></uni-icons>
</view>
<view class="action-info">
<text class="action-title">推送消息提醒</text>
<text class="action-desc">向企业发送办理提醒消息</text>
</view>
<uni-icons type="arrowright" size="16" color="#ccc"></uni-icons>
</view>
<view class="action-item" @click="handleAssignWindow">
<view class="action-icon">
<uni-icons type="staff" size="30" color="#52c41a"></uni-icons>
</view>
<view class="action-info">
<text class="action-title">指派办理窗口</text>
<text class="action-desc">指定具体窗口进行办理</text>
</view>
<uni-icons type="arrowright" size="16" color="#ccc"></uni-icons>
</view>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { onLoad, onReady } from '@dcloudio/uni-app'
import boolean from '../../uni_modules/tuniaoui-vue3/libs/async-validator/validator/boolean'
import qiunDataCharts from '@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
const isFromTicket = ref(false)
//
const searchValue = ref('')
const loading = ref(false)
const searched = ref(false)
//
const enterpriseInfo = ref<any>(null)
const showCharts = ref(false)
//
const taxHealth = reactive({
score: 85,
level: '良好',
indicators: {
categories: ["申报及时性", "税款缴纳", "发票管理", "税务合规", "风险防控"],
series: [{
name: "健康度",
data: [90, 85, 80, 75, 95]
}]
}
})
const chartData = ref({
categories: ["申报及时性", "税款缴纳", "发票管理", "税务合规", "风险防控"], //
series: [
{
name: "企业健康度", //
data: [90, 85, 80, 75, 95] //
}
]
})
//
const pendingItems = ref([
{
id: 1,
name: '增值税申报',
deadline: '2024-01-15',
type: 'tax'
},
{
id: 2,
name: '企业所得税预缴',
deadline: '2024-01-20',
type: 'tax'
},
{
id: 3,
name: '社保费用缴纳',
deadline: '2024-01-25',
type: 'social'
}
])
// ()
const opts = reactive({
// uCharts
//
// uCharts
color: ["#1890FF", "#91CB74", "#FAC858", "#EE6666", "#73C0DE", "#3CA272", "#FC8452", "#9A60B4", "#ea7ccc"],
padding: [5, 5, 5, 5],
dataLabel: true,
dataPointShape: false,
enableScroll: false,
legend: {
show: true,
position: "right",
lineHeight: 25
},
extra: {
radar: {
gridType: "circle",
gridColor: "#CCCCCC",
gridCount: 3,
opacity: 1,
max: 100,
labelShow: true,
linearType: "custom",
border: false
}
}
})
//
const initChartData = () => {
//
chartData.value = {
categories: ["申报及时性", "税款缴纳", "发票管理", "税务合规", "风险防控"], //
series: [
{
name: "企业健康度", //
data: [90, 85, 80, 75, 95] //
}
]
}
}
//
const actionPopup = ref()
const currentTask = ref<any>(null)
//
const handleSearch = async () => {
if (!searchValue.value.trim()) {
uni.showToast({
title: '请输入票号或验证码',
icon: 'none'
})
return
}
isFromTicket.value = true
loading.value = true
searched.value = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 1000))
//
enterpriseInfo.value = {
name: '某某科技有限公司',
taxId: '91440101MA5XXXXXX',
type: '有限责任公司',
address: '某某市某某区某某路某某号',
legalPerson: '张三'
}
showCharts.value = true
uni.showToast({
title: '查询成功',
icon: 'success'
})
} catch (error) {
console.error('查询失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
} finally {
loading.value = false
}
}
//
const getScoreClass = (score : number) => {
if (score >= 90) return 'score-excellent'
if (score >= 80) return 'score-good'
if (score >= 60) return 'score-fair'
return 'score-poor'
}
//
const handleGuide = (task : any) => {
currentTask.value = task
actionPopup.value.open()
}
//
const closePopup = () => {
actionPopup.value.close()
}
//
const handlePushMessage = () => {
closePopup()
uni.showModal({
title: '推送消息',
content: `确定要向企业推送"${currentTask.value.name}"的办理提醒吗?`,
success: (res) => {
if (res.confirm) {
// API
uni.showToast({
title: '消息推送成功',
icon: 'success'
})
}
}
})
}
//
const handleAssignWindow = () => {
closePopup()
//
uni.showActionSheet({
itemList: ['1号窗口', '2号窗口', '3号窗口', '4号窗口'],
success: (res) => {
const windowIndex = res.tapIndex + 1
uni.showToast({
title: `已指派${windowIndex}号窗口`,
icon: 'success'
})
}
})
}
onLoad((option) => {
console.log(typeof (option.isFromTicket))
if(option.tktId) {
searchValue.value = option.tktId
handleSearch()
}
isFromTicket.value = option.isFromTicket === 'true'
})
//
onReady(() => {
//
// initCharts()
})
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #fff;
}
.content {
padding: 6vh 20px 0 20px;
}
//
.search-section {
margin-bottom: 30rpx;
.search-card {
display: flex;
gap: 20rpx;
align-items: center;
.search-btn {
width: 200rpx;
height: 32px;
}
}
}
//
.section-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.item-count {
font-size: 24rpx;
color: #999;
}
}
//
.enterprise-info {
.info-row {
display: flex;
margin-bottom: 20rpx;
.info-label {
width: 200rpx;
font-size: 28rpx;
color: #666;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
//
.charts-container {
display: flex;
flex-wrap: wrap;
gap: 30rpx;
.chart-item {
flex: 1;
min-width: 300rpx;
text-align: center;
.chart-title {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.score-ring {
position: relative;
width: 200rpx;
height: 200rpx;
margin: 0 auto 20rpx;
border: 8rpx solid #e8f4ff;
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.score-text {
font-size: 48rpx;
font-weight: bold;
color: #0099ff;
}
.score-label {
font-size: 24rpx;
color: #999;
}
}
.score-level {
font-size: 28rpx;
font-weight: 500;
&.score-excellent {
color: #52c41a;
}
&.score-good {
color: #0099ff;
}
&.score-fair {
color: #faad14;
}
&.score-poor {
color: #ff4d4f;
}
}
.chart-canvas {
margin-left: 60rpx;
width: 900rpx;
height: 150px;
// background-color: #faad14;
}
}
}
//
.pending-list {
.pending-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.item-main {
flex: 1;
.item-name {
display: block;
font-size: 30rpx;
color: #333;
margin-bottom: 10rpx;
}
.item-deadline {
font-size: 24rpx;
color: #ff4d4f;
}
}
}
}
//
.empty-section {
text-align: center;
padding: 100rpx 0;
.empty-text {
display: block;
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
//
.action-popup {
.popup-content {
width: 600rpx;
border-radius: 20rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.popup-body {
padding: 30rpx;
.current-task {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
text-align: center;
}
.action-options {
.action-item {
display: flex;
align-items: center;
padding: 30rpx;
border: 1rpx solid #f0f0f0;
border-radius: 16rpx;
margin-bottom: 20rpx;
&:active {
background-color: #f8f8f8;
}
.action-icon {
margin-right: 20rpx;
}
.action-info {
flex: 1;
.action-title {
display: block;
font-size: 30rpx;
color: #333;
margin-bottom: 8rpx;
}
.action-desc {
font-size: 24rpx;
color: #999;
}
}
}
}
}
}
</style>

@ -0,0 +1,723 @@
<template>
<view class="page-container">
<div class="top-div">
<div></div>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<view class="content">
<!-- 窗口状态监控 -->
<view class="status-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">窗口状态监控</text>
<view class="status-summary">
<text class="summary-item">空闲窗口: {{ freeWindowsCount }}</text>
<text class="summary-item">忙碌窗口: {{ busyWindowsCount }}</text>
</view>
</view>
<!-- 人工窗口 -->
<view class="window-group">
<view class="group-title">
<uni-icons type="staff" size="20" color="#1890ff"></uni-icons>
<text>人工服务窗口</text>
</view>
<view class="windows-grid">
<view v-for="window in manualWindows" :key="window.id" class="window-item"
:class="getWindowStatusClass(window.status)" @click="handleWindowClick(window)">
<view class="window-header">
<text class="window-number">{{ window.number }}</text>
<view class="window-status" :class="'status-' + window.status">
{{ getWindowStatusText(window.status) }}
</view>
</view>
<view class="window-info">
<text class="window-type">{{ window.type }}</text>
<text class="waiting-count">等候: {{ window.waitingCount }}</text>
</view>
<view class="window-current">
<text class="current-task" v-if="window.currentTask">
当前: {{ window.currentTask }}
</text>
<text class="no-task" v-else></text>
</view>
</view>
</view>
</view>
<!-- 自助设备 -->
<view class="window-group">
<view class="group-title">
<uni-icons type="settings" size="20" color="#52c41a"></uni-icons>
<text>自助办税设备</text>
</view>
<view class="devices-grid">
<view v-for="device in selfServiceDevices" :key="device.id" class="device-item"
:class="getDeviceStatusClass(device.status)">
<view class="device-icon">
<uni-icons :type="device.icon" size="30"
:color="getDeviceIconColor(device.status)"></uni-icons>
</view>
<view class="device-info">
<text class="device-name">{{ device.name }}</text>
<text class="device-status" :class="'status-' + device.status">
{{ getDeviceStatusText(device.status) }}
</text>
<text class="device-usage" v-if="device.status === 'busy'">
使用中
</text>
<text class="device-usage" v-else>
空闲
</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 窗口详情弹窗 -->
<uni-popup ref="windowPopup" type="center" background-color="#fff">
<view class="popup-content" v-if="selectedWindow">
<view class="popup-header">
<text class="popup-title">窗口详情 - {{ selectedWindow.number }}</text>
<uni-icons type="close" size="20" color="#999" @click="closeWindowPopup"></uni-icons>
</view>
<view class="popup-body">
<view class="detail-grid">
<view class="detail-item">
<text class="detail-label">窗口编号</text>
<text class="detail-value">{{ selectedWindow.number }}</text>
</view>
<view class="detail-item">
<text class="detail-label">窗口类型</text>
<text class="detail-value">{{ selectedWindow.type }}</text>
</view>
<view class="detail-item">
<text class="detail-label">当前状态</text>
<view class="detail-value">
<view class="status-badge" :class="'status-' + selectedWindow.status">
{{ getWindowStatusText(selectedWindow.status) }}
</view>
</view>
</view>
<view class="detail-item">
<text class="detail-label">等候人数</text>
<text class="detail-value">{{ selectedWindow.waitingCount }}</text>
</view>
<view class="detail-item">
<text class="detail-label">当前业务</text>
<text class="detail-value">{{ selectedWindow.currentTask || '无' }}</text>
</view>
<view class="detail-item">
<text class="detail-label">工作人员</text>
<text class="detail-value">{{ selectedWindow.operator || '未分配' }}</text>
</view>
</view>
<view class="waiting-list" v-if="selectedWindow.waitingCount > 0">
<view class="list-title">等候队列</view>
<view class="list-items">
<view v-for="(person, index) in getWaitingList(selectedWindow.waitingCount)" :key="index"
class="list-item">
<text class="queue-number">{{ index + 1 }}</text>
<text class="estimate-time">预计等待: {{ (index + 1) * 5 }}分钟</text>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<tn-button type="primary" @click="handleWindowAction(selectedWindow)">
<uni-icons type="gear" size="16" color="#fff"></uni-icons>
窗口操作
</tn-button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
//
const windowPopup = ref()
//
const manualWindows = ref([{
id: 1,
number: 'A001',
type: '综合业务',
status: 'busy',
waitingCount: 5,
currentTask: '增值税申报',
operator: '张三'
},
{
id: 2,
number: 'A002',
type: '发票办理',
status: 'free',
waitingCount: 2,
currentTask: '',
operator: '李四'
},
{
id: 3,
number: 'A003',
type: '税务登记',
status: 'busy',
waitingCount: 3,
currentTask: '新办企业登记',
operator: '王五'
},
{
id: 4,
number: 'A004',
type: '社保缴纳',
status: 'free',
waitingCount: 0,
currentTask: '',
operator: '赵六'
},
{
id: 5,
number: 'A005',
type: '个税业务',
status: 'pause',
waitingCount: 4,
currentTask: '个税汇算清缴',
operator: '钱七'
},
{
id: 6,
number: 'A006',
type: '咨询导税',
status: 'free',
waitingCount: 1,
currentTask: '',
operator: '孙八'
}
])
//
const selfServiceDevices = ref([{
id: 1,
name: '自助发票申领机',
status: 'free',
icon: 'printer'
},
{
id: 2,
name: '自助代开发票机',
status: 'busy',
icon: 'document'
},
{
id: 3,
name: '自助查询终端',
status: 'free',
icon: 'search'
},
{
id: 4,
name: '自助申报设备',
status: 'maintenance',
icon: 'cloud-upload'
},
{
id: 5,
name: '自助打印终端',
status: 'free',
icon: 'paperplane'
},
{
id: 6,
name: '自助缴税设备',
status: 'busy',
icon: 'card'
}
])
//
const freeWindowsCount = computed(() => {
return manualWindows.value.filter(window => window.status === 'free').length
})
const busyWindowsCount = computed(() => {
return manualWindows.value.filter(window => window.status === 'busy').length
})
//
const editing = computed(() => {
return hallParams.value.some(param => param.editing)
})
//
const selectedWindow = ref<any>(null)
//
const getWindowStatusClass = (status : string) => {
const classMap : any = {
free: 'window-free',
busy: 'window-busy',
pause: 'window-pause'
}
return classMap[status] || 'window-free'
}
const getWindowStatusText = (status : string) => {
const textMap : any = {
free: '空闲',
busy: '忙碌',
pause: '暂停'
}
return textMap[status] || '未知'
}
//
const getDeviceStatusClass = (status : string) => {
const classMap : any = {
free: 'device-free',
busy: 'device-busy',
maintenance: 'device-maintenance'
}
return classMap[status] || 'device-free'
}
const getDeviceStatusText = (status : string) => {
const textMap : any = {
free: '空闲',
busy: '使用中',
maintenance: '维护中'
}
return textMap[status] || '未知'
}
const getDeviceIconColor = (status : string) => {
const colorMap : any = {
free: '#52c41a',
busy: '#fa541c',
maintenance: '#faad14'
}
return colorMap[status] || '#52c41a'
}
//
const handleWindowClick = (window : any) => {
selectedWindow.value = window
windowPopup.value.open()
}
const closeWindowPopup = () => {
windowPopup.value.close()
}
//
const getWaitingList = (count : number) => {
return Array.from({
length: count
}, (_, index) => ({
id: index + 1,
queueNumber: index + 1
}))
}
//
const handleWindowAction = (window : any) => {
uni.showActionSheet({
itemList: ['暂停服务', '重置窗口', '呼叫下一位', '查看统计'],
success: (res) => {
const actions = ['pause', 'reset', 'callNext', 'viewStats']
const action = actions[res.tapIndex]
//
uni.showToast({
title: `执行操作: ${action}`,
icon: 'success'
})
}
})
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.top-div {
display: flex;
justify-content: space-between;
background-color: #fff;
padding: 6vh 20px 10px 20px;
}
.content {
padding: 20rpx;
}
//
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.status-summary {
display: flex;
gap: 20rpx;
.summary-item {
font-size: 24rpx;
color: #666;
}
}
}
//
.window-group {
margin-bottom: 40rpx;
.group-title {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
//
.windows-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.window-item {
padding: 24rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
transition: all 0.3s;
&.window-free {
border-color: #52c41a;
background: #f6ffed;
}
&.window-busy {
border-color: #fa541c;
background: #fff2e8;
}
&.window-pause {
border-color: #faad14;
background: #fffbe6;
}
&:active {
transform: scale(0.98);
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
.window-number {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.window-status {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 500;
&.status-free {
background: #52c41a;
color: #fff;
}
&.status-busy {
background: #fa541c;
color: #fff;
}
&.status-pause {
background: #faad14;
color: #fff;
}
}
}
.window-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.window-type {
font-size: 24rpx;
color: #666;
}
.waiting-count {
font-size: 22rpx;
color: #fa541c;
font-weight: 500;
}
}
.window-current {
.current-task {
font-size: 22rpx;
color: #333;
background: #f0f0f0;
padding: 4rpx 8rpx;
border-radius: 6rpx;
}
.no-task {
font-size: 22rpx;
color: #999;
font-style: italic;
}
}
}
}
//
.devices-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
.device-item {
padding: 24rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
text-align: center;
&.device-free {
border-color: #52c41a;
background: #f6ffed;
}
&.device-busy {
border-color: #fa541c;
background: #fff2e8;
}
&.device-maintenance {
border-color: #faad14;
background: #fffbe6;
}
.device-icon {
margin-bottom: 15rpx;
}
.device-info {
.device-name {
display: block;
font-size: 24rpx;
color: #333;
margin-bottom: 8rpx;
}
.device-status {
display: block;
font-size: 20rpx;
margin-bottom: 5rpx;
&.status-free {
color: #52c41a;
}
&.status-busy {
color: #fa541c;
}
&.status-maintenance {
color: #faad14;
}
}
.device-usage {
font-size: 18rpx;
color: #999;
}
}
}
}
//
.param-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.param-value {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
.param-unit {
font-size: 22rpx;
color: #999;
}
}
.param-edit {
display: flex;
justify-content: center;
}
.param-actions {
display: flex;
justify-content: center;
.edit-actions {
display: flex;
gap: 10rpx;
}
}
//
.popup-content {
width: 650rpx;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.popup-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.detail-grid {
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.detail-label {
font-size: 26rpx;
color: #666;
}
.detail-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.status-badge {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
color: #fff;
&.status-free {
background: #52c41a;
}
&.status-busy {
background: #fa541c;
}
&.status-pause {
background: #faad14;
}
}
}
}
.waiting-list {
margin-top: 30rpx;
.list-title {
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
}
.list-items {
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.queue-number {
font-size: 24rpx;
color: #333;
}
.estimate-time {
font-size: 22rpx;
color: #999;
}
}
}
}
.popup-footer {
padding: 30rpx;
border-top: 1rpx solid #f0f0f0;
text-align: center;
}
</style>

@ -0,0 +1,575 @@
<template>
<view class="page-container">
<div class="top-div">
<div></div>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<view class="content">
<!-- 大厅参数管理 -->
<view class="params-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">大厅参数配置</text>
<!-- <tn-button size="sm" type="primary" plain @click="handleEditParams" :disabled="editing">
<uni-icons type="compose" size="16" color="#1890ff"></uni-icons>
{{ editing ? '保存中...' : '编辑参数' }}
</tn-button> -->
</view>
<uni-table border stripe emptyText="暂无参数数据">
<uni-tr>
<uni-th width="200" align="center">参数名称</uni-th>
<uni-th width="250" align="center">参数值</uni-th>
<uni-th width="150" align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(param, index) in hallParams" :key="param.key">
<uni-td align="center">
<text class="param-name">{{ param.name }}</text>
</uni-td>
<uni-td align="center">
<view v-if="!param.editing" class="param-value">
<text>{{ param.value }}</text>
<text class="param-unit" v-if="param.unit">{{ param.unit }}</text>
</view>
<view v-else class="param-edit">
<tn-input v-model="param.editValue" :type="param.inputType || 'text'"
:placeholder="`请输入${param.name}`" size="small" border />
</view>
</uni-td>
<uni-td align="center">
<view class="param-actions">
<tn-button v-if="!param.editing" size="sm" type="primary" plain
@click="startEditParam(index)">
编辑
</tn-button>
<view v-else class="edit-actions">
<tn-button size="sm" type="primary" @click="saveParam(index)">
保存
</tn-button>
<tn-button size="sm" type="default" @click="cancelEditParam(index)">
取消
</tn-button>
</view>
</view>
</uni-td>
</uni-tr>
</uni-table>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
//
const hallParams = ref([
{
key: 'hallName',
name: '大厅名称',
value: '某某市税务局第一办税服务厅',
editing: false,
editValue: '',
inputType: 'text'
},
{
key: 'workTimeStart',
name: '上班时间',
value: '09:00',
editing: false,
editValue: '',
inputType: 'text'
},
{
key: 'workTimeEnd',
name: '下班时间',
value: '17:00',
editing: false,
editValue: '',
inputType: 'text'
},
{
key: 'lunchStart',
name: '午休开始',
value: '12:00',
editing: false,
editValue: '',
inputType: 'text'
},
{
key: 'lunchEnd',
name: '午休结束',
value: '13:30',
editing: false,
editValue: '',
inputType: 'text'
},
{
key: 'maxWaiting',
name: '最大等候人数',
value: '50',
editing: false,
editValue: '',
inputType: 'number',
unit: '人'
},
{
key: 'serviceTimeout',
name: '业务办理超时',
value: '30',
editing: false,
editValue: '',
inputType: 'number',
unit: '分钟'
},
{
key: 'weekendService',
name: '周末服务',
value: '不开放',
editing: false,
editValue: '',
inputType: 'text'
}
])
//
const startEditParam = (index : number) => {
hallParams.value.forEach((param, i) => {
if (i === index) {
param.editing = true
param.editValue = param.value
} else {
param.editing = false
}
})
}
const saveParam = (index : number) => {
const param = hallParams.value[index]
if (param.editValue.trim()) {
param.value = param.editValue
param.editing = false
uni.showToast({
title: '参数保存成功',
icon: 'success'
})
// API
console.log(`保存参数: ${param.name} = ${param.value}`)
} else {
uni.showToast({
title: '参数值不能为空',
icon: 'none'
})
}
}
const cancelEditParam = (index : number) => {
hallParams.value[index].editing = false
hallParams.value[index].editValue = ''
}
//
const handleEditParams = () => {
if (editing.value) {
//
hallParams.value.forEach((param, index) => {
if (param.editing) {
saveParam(index)
}
})
} else {
//
uni.showToast({
title: '进入编辑模式',
icon: 'success'
})
}
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
onMounted(() => {
// API
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.top-div {
display: flex;
justify-content: space-between;
background-color: #fff;
padding: 6vh 20px 10px 20px;
}
.content {
padding: 20rpx;
}
//
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.status-summary {
display: flex;
gap: 20rpx;
.summary-item {
font-size: 24rpx;
color: #666;
}
}
}
//
.window-group {
margin-bottom: 40rpx;
.group-title {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
//
.windows-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.window-item {
padding: 24rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
transition: all 0.3s;
&.window-free {
border-color: #52c41a;
background: #f6ffed;
}
&.window-busy {
border-color: #fa541c;
background: #fff2e8;
}
&.window-pause {
border-color: #faad14;
background: #fffbe6;
}
&:active {
transform: scale(0.98);
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
.window-number {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.window-status {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
font-weight: 500;
&.status-free {
background: #52c41a;
color: #fff;
}
&.status-busy {
background: #fa541c;
color: #fff;
}
&.status-pause {
background: #faad14;
color: #fff;
}
}
}
.window-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.window-type {
font-size: 24rpx;
color: #666;
}
.waiting-count {
font-size: 22rpx;
color: #fa541c;
font-weight: 500;
}
}
.window-current {
.current-task {
font-size: 22rpx;
color: #333;
background: #f0f0f0;
padding: 4rpx 8rpx;
border-radius: 6rpx;
}
.no-task {
font-size: 22rpx;
color: #999;
font-style: italic;
}
}
}
}
//
.devices-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
.device-item {
padding: 24rpx;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
text-align: center;
&.device-free {
border-color: #52c41a;
background: #f6ffed;
}
&.device-busy {
border-color: #fa541c;
background: #fff2e8;
}
&.device-maintenance {
border-color: #faad14;
background: #fffbe6;
}
.device-icon {
margin-bottom: 15rpx;
}
.device-info {
.device-name {
display: block;
font-size: 24rpx;
color: #333;
margin-bottom: 8rpx;
}
.device-status {
display: block;
font-size: 20rpx;
margin-bottom: 5rpx;
&.status-free {
color: #52c41a;
}
&.status-busy {
color: #fa541c;
}
&.status-maintenance {
color: #faad14;
}
}
.device-usage {
font-size: 18rpx;
color: #999;
}
}
}
}
//
.param-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.param-value {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
.param-unit {
font-size: 22rpx;
color: #999;
}
}
.param-edit {
display: flex;
justify-content: center;
}
.param-actions {
display: flex;
justify-content: center;
.edit-actions {
display: flex;
gap: 10rpx;
}
}
//
.popup-content {
width: 650rpx;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.popup-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.detail-grid {
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.detail-label {
font-size: 26rpx;
color: #666;
}
.detail-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
.status-badge {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
color: #fff;
&.status-free {
background: #52c41a;
}
&.status-busy {
background: #fa541c;
}
&.status-pause {
background: #faad14;
}
}
}
}
.waiting-list {
margin-top: 30rpx;
.list-title {
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
}
.list-items {
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.queue-number {
font-size: 24rpx;
color: #333;
}
.estimate-time {
font-size: 22rpx;
color: #999;
}
}
}
}
.popup-footer {
padding: 30rpx;
border-top: 1rpx solid #f0f0f0;
text-align: center;
}
</style>

@ -0,0 +1,637 @@
<template>
<view class="page-container">
<view class="content">
<!-- 搜索区域 -->
<view class="search-section">
<view class="search-card">
<view class="search-row">
<tn-input v-model="searchKeyword" placeholder="请输入网格员姓名或负责区域" border height="80rpx"
class="search-input">
<template #prefix>
<uni-icons type="search" size="20" color="#999"></uni-icons>
</template>
</tn-input>
<tn-button type="primary" @click="handleSearch" :loading="loading" class="search-btn">
搜索
</tn-button>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff"
style="margin-right: 5px;"></uni-icons>
</tn-button>
</view>
</view>
</view>
<!-- 网格员表格 -->
<view class="table-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">网格员列表</text>
<text class="total-count"> {{ gridMembers.length }} </text>
</view>
<uni-table ref="table" :loading="loading" border stripe emptyText="暂无网格员数据">
<uni-tr>
<uni-th width="120" align="center">姓名</uni-th>
<uni-th width="150" align="center">工号</uni-th>
<uni-th width="200" align="center">负责区域</uni-th>
<uni-th width="120" align="center">联系方式</uni-th>
<uni-th width="100" align="center">在岗状态</uni-th>
<uni-th width="120" align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(member, index) in gridMembers" :key="member.id">
<uni-td align="center">
<view class="member-info">
<image :src="member.avatar || '/static/default-avatar.png'" class="avatar"></image>
<text class="member-name">{{ member.name }}</text>
</view>
</uni-td>
<uni-td align="center">{{ member.workNumber }}</uni-td>
<uni-td align="center">
<text class="area-text">{{ member.area }}</text>
</uni-td>
<uni-td align="center">
<view class="contact-info">
<text class="phone">{{ member.phone }}</text>
<text class="email" v-if="member.email">{{ member.email }}</text>
</view>
</uni-td>
<uni-td align="center">
<view class="status-badge" :class="getStatusClass(member.status)">
{{ getStatusText(member.status) }}
</view>
</uni-td>
<uni-td align="center">
<tn-button size="sm" type="primary" plain @click="viewMemberDetail(member)">
查看详情
</tn-button>
</uni-td>
</uni-tr>
</uni-table>
<!-- 分页 -->
<view class="pagination-section" v-if="total > pageSize">
<uni-pagination :total="total" :pageSize="pageSize" :current="currentPage"
@change="handlePageChange" show-icon />
</view>
</view>
</view>
</view>
<!-- 网格员详情弹窗 -->
<uni-popup ref="detailPopup" type="center" background-color="#fff" class="detail-popup">
<view class="popup-content" v-if="currentMember">
<view class="popup-header">
<text class="popup-title">网格员详细信息</text>
<uni-icons type="close" size="20" color="#999" @click="closeDetailPopup"></uni-icons>
</view>
<view class="popup-body">
<!-- 基本信息 -->
<view class="detail-section">
<view class="detail-header">
<text class="detail-title">基本信息</text>
</view>
<view class="detail-grid">
<view class="detail-item">
<text class="item-label">姓名</text>
<text class="item-value">{{ currentMember.name }}</text>
</view>
<view class="detail-item">
<text class="item-label">工号</text>
<text class="item-value">{{ currentMember.workNumber }}</text>
</view>
<view class="detail-item">
<text class="item-label">性别</text>
<text class="item-value">{{ currentMember.gender }}</text>
</view>
<view class="detail-item">
<text class="item-label">职务</text>
<text class="item-value">{{ currentMember.position }}</text>
</view>
</view>
</view>
<!-- 联系信息 -->
<view class="detail-section">
<view class="detail-header">
<text class="detail-title">联系信息</text>
</view>
<view class="detail-grid">
<view class="detail-item">
<text class="item-label">手机号码</text>
<text class="item-value">{{ currentMember.phone }}</text>
</view>
<view class="detail-item">
<text class="item-label">电子邮箱</text>
<text class="item-value">{{ currentMember.email || '未填写' }}</text>
</view>
<view class="detail-item">
<text class="item-label">办公电话</text>
<text class="item-value">{{ currentMember.officePhone || '未填写' }}</text>
</view>
</view>
</view>
<!-- 工作信息 -->
<view class="detail-section">
<view class="detail-header">
<text class="detail-title">工作信息</text>
</view>
<view class="detail-grid">
<view class="detail-item full-width">
<text class="item-label">负责区域</text>
<text class="item-value">{{ currentMember.area }}</text>
</view>
<view class="detail-item">
<text class="item-label">在岗状态</text>
<view class="status-badge" :class="getStatusClass(currentMember.status)">
{{ getStatusText(currentMember.status) }}
</view>
</view>
<view class="detail-item">
<text class="item-label">入职时间</text>
<text class="item-value">{{ currentMember.joinDate }}</text>
</view>
<view class="detail-item full-width">
<text class="item-label">管辖企业数量</text>
<text class="item-value">{{ currentMember.enterpriseCount }} </text>
</view>
</view>
</view>
<!-- 工作统计 -->
<view class="detail-section">
<view class="detail-header">
<text class="detail-title">工作统计</text>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{ currentMember.monthlyTasks || 0 }}</text>
<text class="stat-label">本月任务</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ currentMember.completedTasks || 0 }}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ currentMember.responseRate || '0%' }}</text>
<text class="stat-label">响应率</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ currentMember.satisfaction || '0%' }}</text>
<text class="stat-label">满意度</text>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<tn-button type="primary" @click="contactMember(currentMember)" class="action-btn">
<uni-icons type="phone" size="16" color="#fff"></uni-icons>
联系网格员
</tn-button>
<tn-button type="default" @click="assignTask(currentMember)" class="action-btn">
<uni-icons type="plus" size="16" color="#666"></uni-icons>
分配任务
</tn-button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
//
const searchKeyword = ref('')
const loading = ref(false)
//
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
//
const detailPopup = ref()
//
const gridMembers = ref<any[]>([])
const currentMember = ref<any>(null)
//
const statusMap = {
1: { text: '在岗', class: 'status-online' },
2: { text: '离岗', class: 'status-offline' },
3: { text: '忙碌', class: 'status-busy' },
4: { text: '休假', class: 'status-leave' }
}
//
const initGridMembers = () => {
// - API
gridMembers.value = [
{
id: 1,
name: '张三',
workNumber: 'GY2024001',
area: '某某区某某街道税务网格A区',
phone: '13800138001',
email: 'zhangsan@tax.gov.cn',
status: 1,
gender: '男',
position: '高级网格员',
officePhone: '020-12345678',
joinDate: '2022-03-15',
enterpriseCount: 45,
monthlyTasks: 23,
completedTasks: 18,
responseRate: '95%',
satisfaction: '92%'
},
{
id: 2,
name: '李四',
workNumber: 'GY2024002',
area: '某某区某某街道税务网格B区',
phone: '13800138002',
email: 'lisi@tax.gov.cn',
status: 2,
gender: '女',
position: '网格员',
officePhone: '020-12345679',
joinDate: '2023-01-10',
enterpriseCount: 38,
monthlyTasks: 19,
completedTasks: 15,
responseRate: '88%',
satisfaction: '90%'
},
{
id: 3,
name: '王五',
workNumber: 'GY2024003',
area: '某某区某某街道税务网格C区',
phone: '13800138003',
status: 3,
gender: '男',
position: '网格组长',
joinDate: '2021-08-20',
enterpriseCount: 52,
monthlyTasks: 28,
completedTasks: 25,
responseRate: '98%',
satisfaction: '96%'
},
{
id: 4,
name: '赵六',
workNumber: 'GY2024004',
area: '某某区某某街道税务网格D区',
phone: '13800138004',
email: 'zhaoliu@tax.gov.cn',
status: 4,
gender: '女',
position: '网格员',
joinDate: '2023-05-08',
enterpriseCount: 32,
monthlyTasks: 16,
completedTasks: 12,
responseRate: '85%',
satisfaction: '87%'
}
]
total.value = gridMembers.value.length
}
//
const handleSearch = () => {
loading.value = true
// - API
setTimeout(() => {
if (searchKeyword.value) {
const filtered = gridMembers.value.filter(member =>
member.name.includes(searchKeyword.value) ||
member.area.includes(searchKeyword.value)
)
gridMembers.value = filtered
total.value = filtered.length
} else {
initGridMembers()
}
loading.value = false
}, 500)
}
//
const getStatusClass = (status : number) => {
return statusMap[status]?.class || 'status-offline'
}
//
const getStatusText = (status : number) => {
return statusMap[status]?.text || '未知'
}
//
const viewMemberDetail = (member : any) => {
currentMember.value = member
detailPopup.value.open()
}
//
const closeDetailPopup = () => {
detailPopup.value.close()
}
//
const contactMember = (member : any) => {
uni.makePhoneCall({
phoneNumber: member.phone
})
}
//
const assignTask = (member : any) => {
uni.showModal({
title: '分配任务',
content: `确定要给 ${member.name} 分配新任务吗?`,
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: `/pages/task/assign?memberId=${member.id}`
})
closeDetailPopup()
}
}
})
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
//
const handlePageChange = (e : any) => {
currentPage.value = e.current
// API
}
onMounted(() => {
initGridMembers()
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.content {
padding: 6vh 20px 0 20px;
}
//
.search-section {
margin-bottom: 30rpx;
.search-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.search-row {
display: flex;
gap: 20rpx;
align-items: center;
.search-input {
flex: 1;
}
.search-btn {
width: 200rpx;
height: 32px;
}
}
}
}
//
.table-section {
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.total-count {
font-size: 24rpx;
color: #999;
}
}
}
//
.member-info {
display: flex;
align-items: center;
justify-content: center;
.avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
margin-right: 15rpx;
}
.member-name {
font-size: 28rpx;
color: #333;
}
}
.area-text {
font-size: 26rpx;
color: #666;
line-height: 1.4;
}
.contact-info {
.phone {
display: block;
font-size: 26rpx;
color: #333;
}
.email {
display: block;
font-size: 22rpx;
color: #999;
margin-top: 5rpx;
}
}
//
.status-badge {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
&.status-online {
background: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
&.status-offline {
background: #f5f5f5;
color: #999;
border: 1rpx solid #d9d9d9;
}
&.status-busy {
background: #fff2e8;
color: #fa541c;
border: 1rpx solid #ffbb96;
}
&.status-leave {
background: #e6f7ff;
color: #1890ff;
border: 1rpx solid #91d5ff;
}
}
//
.pagination-section {
margin-top: 30rpx;
display: flex;
justify-content: center;
}
//
.detail-popup {
.popup-content {
width: 700rpx;
max-height: 80vh;
border-radius: 20rpx;
overflow: hidden;
background: #fff;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.popup-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.detail-section {
margin-bottom: 40rpx;
.detail-header {
margin-bottom: 20rpx;
padding-bottom: 15rpx;
border-bottom: 1rpx solid #f0f0f0;
.detail-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
}
.detail-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.detail-item {
width: calc(50% - 10rpx);
&.full-width {
width: 100%;
}
.item-label {
font-size: 26rpx;
color: #666;
}
.item-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
}
.stats-grid {
display: flex;
justify-content: space-around;
text-align: center;
.stat-item {
.stat-number {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #1890ff;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #999;
}
}
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1rpx solid #f0f0f0;
.action-btn {
flex: 1;
}
}
}
</style>

@ -0,0 +1,916 @@
<template>
<view class="page-container">
<view class="content">
<!-- 查询区域 -->
<view class="search-section">
<view class="search-card">
<view class="input-row">
<tn-input v-model="searchValue" :placeholder="placeholderText" border height="80rpx"
class="search-input">
</tn-input>
<tn-button type="primary" @click="handleSearch" :loading="loading" class="search-btn">
查询
</tn-button>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</view>
</view>
</view>
<!-- 纳税人基础信息 -->
<view v-if="taxPayerInfo" class="info-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">纳税人基础信息</text>
<view class="status-badges">
<view class="status-badge" :class="getTaxStatusClass(taxPayerInfo.taxStatus)">
{{ taxPayerInfo.taxStatus }}
</view>
<view class="status-badge" :class="getCreditLevelClass(taxPayerInfo.creditLevel)">
信用{{ taxPayerInfo.creditLevel }}
</view>
</view>
</view>
<view class="info-grid">
<view class="info-group">
<view class="group-title">登记信息</view>
<view class="info-row">
<text class="info-label">纳税人名称</text>
<text class="info-value">{{ taxPayerInfo.name }}</text>
</view>
<view class="info-row">
<text class="info-label">纳税人识别号</text>
<text class="info-value">{{ taxPayerInfo.taxId }}</text>
</view>
<view class="info-row">
<text class="info-label">登记注册类型</text>
<text class="info-value">{{ taxPayerInfo.registerType }}</text>
</view>
<view class="info-row">
<text class="info-label">行业类型</text>
<text class="info-value">{{ taxPayerInfo.industry }}</text>
</view>
<view class="info-row">
<text class="info-label">主管税务机关</text>
<text class="info-value">{{ taxPayerInfo.taxAuthority }}</text>
</view>
</view>
<view class="info-group">
<view class="group-title">人员信息</view>
<view class="info-row">
<text class="info-label">法定代表人</text>
<text class="info-value">{{ taxPayerInfo.legalPerson }}</text>
</view>
<view class="info-row">
<text class="info-label">财务负责人</text>
<text class="info-value">{{ taxPayerInfo.financialManager }}</text>
</view>
<view class="info-row">
<text class="info-label">办税人员</text>
<text class="info-value">{{ taxPayerInfo.taxAgent }}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话</text>
<text class="info-value">{{ taxPayerInfo.contactPhone }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 申报缴税信息 -->
<view v-if="declarationRecords.length > 0" class="declaration-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">申报缴税信息</text>
<view class="section-actions">
<!-- <tn-button size="sm" type="primary" plain @click="exportDeclarationData">
<uni-icons type="download" size="16" color="#1890ff"></uni-icons>
导出数据
</tn-button> -->
</view>
</view>
<view class="time-filter">
<tn-button v-for="period in timePeriods" :key="period.value" size="sm"
:type="currentPeriod === period.value ? 'primary' : 'default'"
@click="changeTimePeriod(period.value)">
{{ period.label }}
</tn-button>
</view>
<uni-table border stripe emptyText="暂无申报数据">
<uni-tr>
<uni-th width="120" align="center">税款所属期</uni-th>
<uni-th width="100" align="center">税种</uni-th>
<uni-th width="100" align="center">申报期限</uni-th>
<uni-th width="100" align="center">申报状态</uni-th>
<uni-th width="120" align="center">应纳税额</uni-th>
<uni-th width="120" align="center">已缴税额</uni-th>
<uni-th width="100" align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(record, index) in filteredDeclarationRecords" :key="index">
<uni-td align="center">{{ record.taxPeriod }}</uni-td>
<uni-td align="center">{{ record.taxType }}</uni-td>
<uni-td align="center">{{ record.deadline }}</uni-td>
<uni-td align="center">
<view class="status-badge" :class="getDeclarationStatusClass(record.status)">
{{ record.status }}
</view>
</uni-td>
<uni-td align="center">¥{{ record.taxPayable }}</uni-td>
<uni-td align="center">¥{{ record.taxPaid }}</uni-td>
<uni-td align="center">
<tn-button size="sm" type="primary" plain @click="viewDeclarationDetail(record)">
查看
</tn-button>
</uni-td>
</uni-tr>
</uni-table>
</view>
</view>
<!-- 财务报表数据 -->
<view v-if="financialReports.length > 0" class="financial-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">财务报表数据</text>
<text class="report-period">报表期间: {{ currentReportPeriod }}</text>
</view>
<view class="reports-tabs">
<tn-tabs v-model="currentReportType" :list="reportTypes" @change="onReportTypeChange"></tn-tabs>
</view>
<view class="financial-data">
<view class="balance-sheet" v-if="currentReportType === 0">
<view class="table-title">资产负债表</view>
<uni-table border size="small">
<uni-tr>
<uni-th width="200" align="left">资产项目</uni-th>
<uni-th width="150" align="right">期末余额</uni-th>
<uni-th width="200" align="left">负债和权益项目</uni-th>
<uni-th width="150" align="right">期末余额</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in balanceSheet" :key="index">
<uni-td align="left">{{ item.assetItem }}</uni-td>
<uni-td align="right">{{ item.assetAmount }}</uni-td>
<uni-td align="left">{{ item.liabilityItem }}</uni-td>
<uni-td align="right">{{ item.liabilityAmount }}</uni-td>
</uni-tr>
</uni-table>
</view>
<view class="profit-table" v-if="currentReportType === 1">
<view class="table-title">利润表</view>
<uni-table border size="small">
<uni-tr>
<uni-th width="250" align="left">项目</uni-th>
<uni-th width="150" align="right">本期金额</uni-th>
<uni-th width="150" align="right">上期金额</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in profitStatement" :key="index">
<uni-td align="left">{{ item.item }}</uni-td>
<uni-td align="right">{{ item.currentAmount }}</uni-td>
<uni-td align="right">{{ item.previousAmount }}</uni-td>
</uni-tr>
</uni-table>
</view>
</view>
</view>
</view>
<!-- 发票使用信息 -->
<view v-if="invoiceStats" class="invoice-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">发票使用信息</text>
<view class="invoice-stats">
<text class="stat-item">本年开票: {{ invoiceStats.yearIssued }}</text>
<text class="stat-item">本年收票: {{ invoiceStats.yearReceived }}</text>
</view>
</view>
<view class="invoice-overview">
<view class="overview-item">
<text class="overview-value">{{ invoiceStats.totalIssued }}</text>
<text class="overview-label">累计开具</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ invoiceStats.totalReceived }}</text>
<text class="overview-label">累计取得</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ invoiceStats.creditAmount }}</text>
<text class="overview-label">进项税额</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ invoiceStats.taxAmount }}</text>
<text class="overview-label">销项税额</text>
</view>
</view>
<view class="invoice-actions">
<tn-button size="sm" type="primary" plain @click="viewInvoiceList('issued')">
查看开票记录
</tn-button>
<tn-button size="sm" type="primary" plain @click="viewInvoiceList('received')">
查看收票记录
</tn-button>
<tn-button size="sm" type="primary" plain @click="invoiceVerification">
发票查验
</tn-button>
</view>
</view>
</view>
<!-- 纳税评估信息 -->
<view v-if="taxAssessment" class="assessment-section">
<view class="section-card">
<view class="section-header">
<text class="section-title">纳税评估信息</text>
<view class="risk-level" :class="getRiskLevelClass(taxAssessment.riskLevel)">
风险等级: {{ taxAssessment.riskLevel }}
</view>
</view>
<view class="assessment-content">
<view class="risk-indicators">
<view class="indicator-item" v-for="indicator in taxAssessment.riskIndicators"
:key="indicator.name">
<text class="indicator-name">{{ indicator.name }}</text>
<view class="indicator-bar">
<view class="indicator-progress" :class="'level-' + indicator.level"
:style="{ width: indicator.value + '%' }"></view>
</view>
<text class="indicator-value">{{ indicator.value }}%</text>
</view>
</view>
<view class="assessment-notes" v-if="taxAssessment.notes">
<view class="notes-title">评估说明</view>
<text class="notes-content">{{ taxAssessment.notes }}</text>
</view>
<view class="suggestions" v-if="taxAssessment.suggestions">
<view class="suggestions-title">处理建议</view>
<text class="suggestions-content">{{ taxAssessment.suggestions }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="searched && !taxPayerInfo" class="empty-section">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">未找到对应的纳税人信息</text>
<text class="empty-tips">请检查查询条件是否正确</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { onLoad, onReady } from '@dcloudio/uni-app'
//
const searchType = ref(0) // 0:, 1:
const searchValue = ref('')
const loading = ref(false)
const searched = ref(false)
const tabList = [
{ name: '票号查询' },
{ name: '手机号查询' }
]
const placeholderText = computed(() => {
return searchType.value === 0 ? '请输入纳税人票号' : '请输入手机号码'
})
//
const taxPayerInfo = ref<any>(null)
//
const declarationRecords = ref<any[]>([])
const currentPeriod = ref('currentYear')
const timePeriods = [
{ label: '本年度', value: 'currentYear' },
{ label: '上年度', value: 'lastYear' },
{ label: '本季度', value: 'currentQuarter' },
{ label: '自定义', value: 'custom' }
]
const filteredDeclarationRecords = computed(() => {
//
return declarationRecords.value.filter(record => {
//
return true
})
})
//
const financialReports = ref<any[]>([])
const currentReportType = ref(0)
const currentReportPeriod = ref('2024年第三季度')
const reportTypes = [
{ name: '资产负债表' },
{ name: '利润表' },
{ name: '现金流量表' }
]
//
const invoiceStats = ref<any>(null)
//
const taxAssessment = ref<any>(null)
//
const onTabChange = (index : number) => {
searchType.value = index
searchValue.value = ''
resetData()
}
//
const handleSearch = async () => {
if (!searchValue.value.trim()) {
uni.showToast({
title: `请输入${searchType.value === 0 ? '票号' : '手机号'}`,
icon: 'none'
})
return
}
loading.value = true
searched.value = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 1500))
//
taxPayerInfo.value = {
name: '某某科技有限公司',
taxId: '91440101MA5XXXXXX',
registerType: '有限责任公司',
industry: '软件和信息技术服务业',
taxAuthority: '某某市税务局第一分局',
taxStatus: '正常',
creditLevel: 'A',
legalPerson: '张三',
financialManager: '李四',
taxAgent: '王五',
contactPhone: '13800138000'
}
//
initDeclarationData()
initFinancialData()
initInvoiceData()
initAssessmentData()
uni.showToast({
title: '查询成功',
icon: 'success'
})
} catch (error) {
console.error('查询失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
} finally {
loading.value = false
}
}
//
const resetData = () => {
taxPayerInfo.value = null
declarationRecords.value = []
financialReports.value = []
invoiceStats.value = null
taxAssessment.value = null
searched.value = false
}
//
const initDeclarationData = () => {
declarationRecords.value = [
{
taxPeriod: '2024-09',
taxType: '增值税',
deadline: '2024-10-25',
status: '已申报',
taxPayable: '125,680.50',
taxPaid: '125,680.50'
},
{
taxPeriod: '2024-09',
taxType: '企业所得税',
deadline: '2024-10-31',
status: '已申报',
taxPayable: '89,450.00',
taxPaid: '89,450.00'
},
{
taxPeriod: '2024-08',
taxType: '增值税',
deadline: '2024-09-25',
status: '已申报',
taxPayable: '118,920.30',
taxPaid: '118,920.30'
}
]
}
//
const initFinancialData = () => {
//
financialReports.value = [
//
]
}
//
const initInvoiceData = () => {
invoiceStats.value = {
totalIssued: '1,245',
totalReceived: '892',
yearIssued: '356',
yearReceived: '278',
creditAmount: '45.8',
taxAmount: '67.2'
}
}
//
const initAssessmentData = () => {
taxAssessment.value = {
riskLevel: '中风险',
riskIndicators: [
{ name: '申报及时性', value: 85, level: 1 },
{ name: '税款缴纳', value: 92, level: 1 },
{ name: '发票合规', value: 78, level: 2 },
{ name: '财务规范', value: 65, level: 3 }
],
notes: '该纳税人发票使用存在一定风险,建议加强发票管理。',
suggestions: '1. 加强发票真伪查验2. 规范财务核算3. 定期进行税务自查。'
}
}
//
const getTaxStatusClass = (status : string) => {
const classMap : any = {
'正常': 'status-normal',
'非正常': 'status-abnormal',
'注销': 'status-cancelled'
}
return classMap[status] || 'status-normal'
}
const getCreditLevelClass = (level : string) => {
const classMap : any = {
'A': 'credit-a',
'B': 'credit-b',
'C': 'credit-c',
'D': 'credit-d'
}
return classMap[level] || 'credit-a'
}
const getDeclarationStatusClass = (status : string) => {
const classMap : any = {
'已申报': 'status-completed',
'未申报': 'status-pending',
'逾期': 'status-overdue'
}
return classMap[status] || 'status-pending'
}
const getRiskLevelClass = (level : string) => {
const classMap : any = {
'低风险': 'risk-low',
'中风险': 'risk-medium',
'高风险': 'risk-high'
}
return classMap[level] || 'risk-low'
}
//
const changeTimePeriod = (period : string) => {
currentPeriod.value = period
//
}
//
const onReportTypeChange = (index : number) => {
currentReportType.value = index
}
//
const balanceSheet = ref([
{ assetItem: '货币资金', assetAmount: '1,250,000', liabilityItem: '短期借款', liabilityAmount: '500,000' },
{ assetItem: '应收账款', assetAmount: '890,000', liabilityItem: '应付账款', liabilityAmount: '320,000' },
{ assetItem: '存货', assetAmount: '650,000', liabilityItem: '应交税费', liabilityAmount: '125,680' },
{ assetItem: '固定资产', assetAmount: '2,100,000', liabilityItem: '实收资本', liabilityAmount: '3,000,000' },
{ assetItem: '资产总计', assetAmount: '4,890,000', liabilityItem: '负债和权益总计', liabilityAmount: '4,890,000' }
])
//
const profitStatement = ref([
{ item: '营业收入', currentAmount: '2,580,000', previousAmount: '2,350,000' },
{ item: '营业成本', currentAmount: '1,850,000', previousAmount: '1,680,000' },
{ item: '营业利润', currentAmount: '456,000', previousAmount: '412,000' },
{ item: '利润总额', currentAmount: '445,000', previousAmount: '405,000' },
{ item: '净利润', currentAmount: '356,000', previousAmount: '324,000' }
])
//
const exportDeclarationData = () => {
uni.showToast({
title: '数据导出中...',
icon: 'success'
})
}
const viewDeclarationDetail = (record : any) => {
uni.navigateTo({
url: `/pages/declaration/detail?id=${record.taxPeriod}-${record.taxType}`
})
}
const viewInvoiceList = (type : string) => {
uni.navigateTo({
url: `/pages/invoice/list?type=${type}&taxId=${taxPayerInfo.value.taxId}`
})
}
const invoiceVerification = () => {
uni.navigateTo({
url: '/pages/invoice/verification'
})
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
onLoad((options) => {
//
if (options.ticketNo) {
searchType.value = 0
searchValue.value = options.ticketNo
handleSearch()
} else if (options.phone) {
searchType.value = 1
searchValue.value = options.phone
handleSearch()
}
})
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.content {
padding: 6vh 20px 0 20px;
}
//
.search-section {
margin-bottom: 30rpx;
.search-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
.input-row {
display: flex;
gap: 20rpx;
align-items: center;
margin-top: 20rpx;
.search-input {
flex: 1;
}
.search-btn {
width: 200rpx;
height: 32px;
}
}
}
}
//
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
//
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40rpx;
.info-group {
.group-title {
font-size: 28rpx;
font-weight: bold;
color: #1890ff;
margin-bottom: 20rpx;
padding-bottom: 10rpx;
border-bottom: 2rpx solid #1890ff;
}
.info-row {
display: flex;
margin-bottom: 20rpx;
.info-label {
width: 160rpx;
font-size: 26rpx;
color: #666;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
}
}
//
.status-badges {
display: flex;
gap: 15rpx;
}
.status-badge {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
&.status-normal {
background: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
&.credit-a {
background: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
&.credit-b {
background: #e6f7ff;
color: #1890ff;
border: 1rpx solid #91d5ff;
}
&.status-completed {
background: #f6ffed;
color: #52c41a;
}
&.status-pending {
background: #fffbe6;
color: #faad14;
}
}
//
.time-filter {
display: flex;
gap: 15rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
}
.section-actions {
display: flex;
gap: 15rpx;
}
//
.reports-tabs {
margin-bottom: 20rpx;
}
.table-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
text-align: center;
}
//
.invoice-stats {
display: flex;
gap: 20rpx;
.stat-item {
font-size: 24rpx;
color: #666;
}
}
.invoice-overview {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
margin-bottom: 30rpx;
text-align: center;
.overview-item {
.overview-value {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #1890ff;
margin-bottom: 8rpx;
}
.overview-label {
font-size: 22rpx;
color: #999;
}
}
}
.invoice-actions {
display: flex;
gap: 15rpx;
justify-content: center;
}
//
.risk-level {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 500;
&.risk-low {
background: #f6ffed;
color: #52c41a;
border: 1rpx solid #b7eb8f;
}
&.risk-medium {
background: #fffbe6;
color: #faad14;
border: 1rpx solid #ffe58f;
}
&.risk-high {
background: #fff2f0;
color: #ff4d4f;
border: 1rpx solid #ffccc7;
}
}
.risk-indicators {
margin-bottom: 30rpx;
.indicator-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.indicator-name {
width: 200rpx;
font-size: 26rpx;
color: #333;
}
.indicator-bar {
flex: 1;
height: 20rpx;
background: #f0f0f0;
border-radius: 10rpx;
margin: 0 15rpx;
overflow: hidden;
.indicator-progress {
height: 100%;
border-radius: 10rpx;
transition: width 0.3s;
&.level-1 {
background: #52c41a;
}
&.level-2 {
background: #faad14;
}
&.level-3 {
background: #ff4d4f;
}
}
}
.indicator-value {
width: 80rpx;
font-size: 24rpx;
color: #666;
text-align: right;
}
}
}
.assessment-notes,
.suggestions {
margin-bottom: 20rpx;
.notes-title,
.suggestions-title {
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.notes-content,
.suggestions-content {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
}
//
.empty-section {
text-align: center;
padding: 100rpx 0;
.empty-text {
display: block;
margin-top: 20rpx;
font-size: 28rpx;
color: #666;
}
.empty-tips {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
}
//
@media (max-width: 768px) {
.info-grid {
grid-template-columns: 1fr;
gap: 30rpx;
}
.invoice-overview {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

@ -0,0 +1,175 @@
<template>
<view class="push-message-page">
<div class="top-div">
<div></div>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<view class="page-content">
<!-- 推送目标选择 -->
<view class="section">
<view class="section-title">推送目标</view>
<tn-radio-group v-model="pushForm.targetType">
<tn-radio label="all" value="all">全部</tn-radio>
<tn-radio label="ios" value="ios">呼号端</tn-radio>
<tn-radio label="android" value="android">自助机</tn-radio>
<tn-radio label="specific" value="specific">综合屏</tn-radio>
</tn-radio-group>
<!-- 指定设备输入 -->
<view class="specific-device">
<tn-input v-model="pushForm.deviceId" placeholder="请输入设备ID" border></tn-input>
</view>
</view>
<!-- 消息内容编辑 -->
<view class="section">
<view class="section-title">推送内容</view>
<TnInput v-model="pushForm.title" placeholder="请输入推送标题" border class="input-field"></TnInput>
<TnInput v-model="pushForm.content" placeholder="请输入推送内容" border
class="textarea-field"></TnInput>
</view>
<!-- 推送按钮 -->
<view class="button-section">
<tn-button @click="handlePush" :loading="loading" class="push-button">
{{ loading ? '推送中...' : '立即推送' }}
</tn-button>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive
} from 'vue'
import TnInput from '@/uni_modules/tuniaoui-vue3/components/input/src/input.vue'
//
const pushForm = reactive({
targetType: 'all',
deviceId: '',
title: '',
content: ''
})
const loading = ref(false)
//
const handlePush = () => {
//
if (!pushForm.title.trim()) {
uni.showToast({
title: '请输入推送标题',
icon: 'none'
})
return
}
if (!pushForm.content.trim()) {
uni.showToast({
title: '请输入推送内容',
icon: 'none'
})
return
}
if (pushForm.targetType === 'specific' && !pushForm.deviceId.trim()) {
uni.showToast({
title: '请输入设备ID',
icon: 'none'
})
return
}
//
uni.showModal({
title: '确认推送',
content: '确定要发送这条推送消息吗?',
success: (res) => {
if (res.confirm) {
sendPushMessage()
}
}
})
}
//
const sendPushMessage = () => {
console.log('pushing...')
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss" scoped>
.push-message-page {
min-height: 100vh;
background-color: #fff;
.top-div {
display: flex;
justify-content: space-between;
padding: 6vh 20px 1vh 20px;
}
.page-content {
padding: 10px;
}
.section {
background-color: #fff;
border-radius: 3px;
padding: 10px;
margin-bottom: 10px;
// box-shadow: 10px;
.section-title {
font-size: 36rpx;
font-weight: bold;
color: #dddddd;
margin-bottom: 10px;
border-left: 8rpx solid 10px;
padding-left: 10px;
}
}
.specific-device {
margin-top: 10px;
}
.input-field {
margin-bottom: 10px;
}
.textarea-field {
margin-top: 10px;
}
.button-section {
margin-top: 20px;
text-align: center;
.push-button {
width: 80%;
height: 40px;
font-weight: 600;
}
}
}
/* 响应式设计 */
@media (min-width: 768px) {
.push-message-page {
.page-content {
max-width: 600px;
margin: 0 auto;
}
}
}
</style>

@ -0,0 +1,437 @@
<!-- pages/queue/business-select.vue -->
<template>
<view class="business-container">
<!-- 头部信息 -->
<view class="user-info">
<text class="info-title">您好{{ userName }}</text>
<text class="info-subtitle">请选择需要办理的业务类型</text>
</view>
<!-- 业务列表 -->
<scroll-view class="business-list" scroll-y>
<view v-for="(item, index) in businessList" :key="index" class="business-item"
:class="{ 'selected': selectedBusiness === item.value }" @click="selectBusiness(item)">
<view class="business-icon">{{ item.icon }}</view>
<view class="business-content">
<text class="business-name">{{ item.name }}</text>
<text class="business-desc">{{ item.description }}</text>
</view>
<view class="business-check" v-if="selectedBusiness === item.value">
<text></text>
</view>
</view>
</scroll-view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="btn-back" @click="goBack"></button>
<button class="btn-confirm" :disabled="!selectedBusiness" @click="handleConfirm">
确认取号
</button>
</view>
<!-- 取号结果弹窗 -->
<uni-popup ref="resultPopup" type="center" :is-mask-click="false">
<view class="result-popup">
<view class="result-header">
<text class="result-icon"></text>
<text class="result-title">取号成功</text>
</view>
<view class="result-content">
<view class="ticket-info">
<text class="ticket-label">您的排队号码</text>
<text class="ticket-number">{{ ticketInfo.ticketNumber }}</text>
</view>
<view class="ticket-details">
<view class="detail-item">
<text class="detail-label">业务类型</text>
<text class="detail-value">{{ ticketInfo.businessName }}</text>
</view>
<view class="detail-item">
<text class="detail-label">预计等候</text>
<text class="detail-value">{{ ticketInfo.waitingTime }}分钟</text>
</view>
<view class="detail-item">
<text class="detail-label">办理窗口</text>
<text class="detail-value">{{ ticketInfo.serviceWindow }}</text>
</view>
</view>
</view>
<view class="result-actions">
<button class="btn-print" @click="handlePrint"></button>
<button class="btn-notify" @click="handleNotify"></button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
ref
} from 'vue';
import {
onLoad,
onReady
} from '@dcloudio/uni-app'
// import {
// generateTicket,
// printTicket,
// sendNotification
// } from '@/utils/api';
const userName = ref('');
const selectedBusiness = ref('');
const businessList = ref([]);
const ticketInfo = ref({});
const resultPopup = ref(null);
//
onLoad((options) => {
userName.value = decodeURIComponent(options.name || '');
//
businessList.value = [{
icon: '🏦',
name: '税务登记',
value: 'tax_register',
description: '企业税务登记、变更、注销'
},
{
icon: '🧾',
name: '发票办理',
value: 'invoice',
description: '发票申领、开具、验旧'
},
{
icon: '📊',
name: '纳税申报',
value: 'tax_declare',
description: '各类税种申报缴纳'
},
{
icon: '🎯',
name: '税收优惠',
value: 'tax_preference',
description: '优惠政策咨询办理'
},
{
icon: '📝',
name: '证明开具',
value: 'certificate',
description: '完税证明、涉税证明'
},
{
icon: '🔍',
name: '税务查询',
value: 'tax_query',
description: '纳税记录、信用查询'
}
];
});
//
const selectBusiness = (item) => {
selectedBusiness.value = item.value;
};
//
const goBack = () => {
uni.navigateBack();
};
//
const handleConfirm = async () => {
try {
//
const result = await generateTicket({
businessType: selectedBusiness.value
});
if (result.code === 200) {
ticketInfo.value = {
ticketNumber: result.data.ticketNumber,
businessName: businessList.value.find(b => b.value === selectedBusiness.value).name,
waitingTime: result.data.waitingTime || '15-20',
serviceWindow: result.data.serviceWindow || '请等候叫号'
};
//
resultPopup.value.open();
}
} catch (error) {
uni.showToast({
title: error.message || '取号失败',
icon: 'none'
});
}
};
//
const handlePrint = async () => {
uni.showLoading({
title: '正在打印...'
});
try {
const result = await printTicket(ticketInfo.value);
if (result.code === 200) {
uni.showToast({
title: '打印指令已发送',
icon: 'success'
});
}
} catch (error) {
uni.showToast({
title: error.message || '打印失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
//
const handleNotify = async () => {
uni.showLoading({
title: '正在推送...'
});
try {
const result = await sendNotification(ticketInfo.value);
if (result.code === 200) {
uni.showToast({
title: '消息已推送到手机',
icon: 'success'
});
}
} catch (error) {
uni.showToast({
title: error.message || '推送失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
</script>
<style scoped>
.business-container {
padding: 40rpx 32rpx;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.user-info {
text-align: center;
margin-bottom: 40rpx;
}
.info-title {
font-size: 36rpx;
font-weight: bold;
color: #1a1a1a;
display: block;
margin-bottom: 12rpx;
}
.info-subtitle {
font-size: 26rpx;
color: #666;
display: block;
}
.business-list {
height: 60vh;
margin-bottom: 40rpx;
}
.business-item {
background: white;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
border: 2rpx solid #e0e0e0;
transition: all 0.3s ease;
}
.business-item.selected {
border-color: #007AFF;
background: #f0f7ff;
box-shadow: 0 4rpx 20rpx rgba(0, 122, 255, 0.15);
}
.business-icon {
font-size: 48rpx;
margin-right: 24rpx;
}
.business-content {
flex: 1;
}
.business-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.business-desc {
font-size: 24rpx;
color: #666;
display: block;
}
.business-check {
width: 40rpx;
height: 40rpx;
background: #007AFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24rpx;
}
.action-buttons {
display: flex;
gap: 20rpx;
}
.action-buttons button {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;
border: none;
}
.btn-back {
background: #f0f0f0;
color: #666;
}
.btn-confirm {
background: linear-gradient(135deg, #007AFF 0%, #0056CC 100%);
color: white;
}
.btn-confirm:disabled {
background: #cccccc;
opacity: 0.6;
}
/* 取号结果弹窗 */
.result-popup {
width: 650rpx;
max-width: 90vw;
background: white;
border-radius: 24rpx;
overflow: hidden;
}
.result-header {
padding: 40rpx 40rpx 20rpx;
text-align: center;
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
color: white;
}
.result-icon {
font-size: 80rpx;
display: block;
margin-bottom: 16rpx;
}
.result-title {
font-size: 36rpx;
font-weight: bold;
display: block;
}
.result-content {
padding: 40rpx;
}
.ticket-info {
text-align: center;
margin-bottom: 40rpx;
padding-bottom: 40rpx;
border-bottom: 2rpx dashed #e0e0e0;
}
.ticket-label {
font-size: 28rpx;
color: #666;
display: block;
margin-bottom: 16rpx;
}
.ticket-number {
font-size: 72rpx;
font-weight: bold;
color: #007AFF;
display: block;
letter-spacing: 4rpx;
}
.ticket-details {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 28rpx;
color: #666;
}
.detail-value {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.result-actions {
display: flex;
gap: 20rpx;
padding: 0 40rpx 40rpx;
}
.result-actions button {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
border: none;
}
.btn-print {
background: #4CAF50;
color: white;
}
.btn-notify {
background: #2196F3;
color: white;
}
</style>

@ -0,0 +1,433 @@
<!-- pages/queue/index.vue -->
<template>
<view class="queue-container">
<!-- 页面标题 -->
<div class="top-div">
<div></div>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<!-- 表单区域 -->
<view class="form-container">
<form @submit="handleSubmit">
<!-- 姓名输入 -->
<view class="form-item">
<text class="form-label">姓名</text>
<input v-model="formData.name" class="form-input" placeholder="请输入您的真实姓名" type="text" maxlength="10"
@blur="validateName" />
<text v-if="nameError" class="error-text">{{ nameError }}</text>
</view>
<!-- 手机号码输入 -->
<view class="form-item">
<text class="form-label">手机号码</text>
<input v-model="formData.phone" class="form-input" placeholder="请输入11位手机号码" type="number"
maxlength="11" @blur="validatePhone" />
<text v-if="phoneError" class="error-text">{{ phoneError }}</text>
</view>
<!-- 身份证号码输入 -->
<view class="form-item">
<text class="form-label">身份证号码</text>
<IDCardInput v-model="formData.idCard" @change="validateIDCard" />
<text v-if="idCardError" class="error-text">{{ idCardError }}</text>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" :class="{ 'disabled': !formValid }" :disabled="!formValid || isLoading"
form-type="submit">
<text v-if="!isLoading"></text>
<view v-else class="loading-wrapper">
<uni-load-more :status="'loading'" color="#007AFF" size="20"></uni-load-more>
<text>验证中...</text>
</view>
</button>
</form>
</view>
<!-- 实名认证提示弹窗 -->
<uni-popup ref="authPopup" type="center" :is-mask-click="false">
<view class="popup-content auth-popup">
<view class="popup-header">
<text class="popup-title">实名认证提示</text>
</view>
<view class="popup-body">
<text class="auth-icon">🔐</text>
<text class="auth-message">您尚未完成实名认证</text>
<text class="auth-detail">请先完成实名认证后才能办理业务</text>
</view>
<view class="popup-footer">
<button class="popup-btn secondary" @click="closeAuthPopup"></button>
<button class="popup-btn primary" @click="gotoRealNameAuth"></button>
</view>
</view>
</uni-popup>
<!-- 网络错误提示 -->
<uni-popup ref="errorPopup" type="center">
<view class="popup-content error-popup">
<text class="error-icon"></text>
<text class="error-message">{{ errorMessage }}</text>
<button class="error-btn" @click="closeErrorPopup"></button>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
ref,
reactive,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import IDCardInput from '@/components/IDCardInput.vue';
import {
validateChineseName,
validatePhoneNumber,
validateIDCard
} from '@/utils/validator';
// import {
// checkRealNameAuth,
// getBusinessList
// } from '@/utils/api';
//
const formData = reactive({
name: '',
phone: '',
idCard: ''
});
//
const nameError = ref('');
const phoneError = ref('');
const idCardError = ref('');
//
const isLoading = ref(false);
const errorMessage = ref('');
//
const authPopup = ref(null);
const errorPopup = ref(null);
//
const formValid = computed(() => {
return formData.name &&
formData.phone &&
formData.idCard &&
!nameError.value &&
!phoneError.value &&
!idCardError.value;
});
//
const validateName = () => {
const result = validateChineseName(formData.name);
nameError.value = result.valid ? '' : result.message;
};
//
const validatePhone = () => {
const result = validatePhoneNumber(formData.phone);
phoneError.value = result.valid ? '' : result.message;
};
//
const handleIdCardChange = (value, isValid) => {
idCardError.value = isValid ? '' : '请输入正确的身份证号码';
};
//
const handleSubmit = async () => {
//
validateName();
validatePhone();
if (nameError.value || phoneError.value || idCardError.value) {
showError('请检查填写的信息是否正确');
return;
}
isLoading.value = true;
try {
//
uni.navigateTo({
url: `/pages/queue/business-select?name=${encodeURIComponent(formData.name)}&phone=${formData.phone}&idCard=${formData.idCard}`,
success: () => {
//
Object.keys(formData).forEach(key => formData[key] = '');
}
});
} catch (error) {
console.error('实名验证失败:', error);
showError(error.message || '网络异常,请稍后重试');
} finally {
isLoading.value = false;
}
};
//
const showError = (message) => {
errorMessage.value = message;
errorPopup.value.open();
};
//
const closeErrorPopup = () => {
errorPopup.value.close();
};
//
const closeAuthPopup = () => {
authPopup.value.close();
};
//
const gotoRealNameAuth = () => {
authPopup.value.close();
uni.navigateTo({
url: '/pages/auth/real-name'
});
};
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
//
onLoad(() => {
//
if (process.env.NODE_ENV === 'development') {
formData.name = '测试用户';
formData.phone = '13800138000';
formData.idCard = '110101199003077436';
}
});
</script>
<style scoped>
.queue-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.page-header {
text-align: center;
margin-bottom: 60rpx;
}
.top-div {
display: flex;
justify-content: space-between;
background-color: #fff;
padding: 6vh 20px 10px 20px;
}
.page-title {
font-size: 48rpx;
font-weight: bold;
color: #1a1a1a;
display: block;
margin-bottom: 16rpx;
}
.page-subtitle {
font-size: 28rpx;
color: #666;
display: block;
}
.form-container {
flex: 1;
min-height: 100%;
background: white;
padding: 48rpx 40rpx;
}
.form-item {
margin-bottom: 40rpx;
}
.form-label {
display: block;
font-size: 30rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.form-input {
width: 100%;
height: 88rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
background: #fafafa;
box-sizing: border-box;
}
.form-input:focus {
border-color: #007AFF;
background: white;
}
.error-text {
display: block;
font-size: 24rpx;
color: #ff4444;
margin-top: 8rpx;
}
.submit-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #007AFF 0%, #0056CC 100%);
color: white;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
margin-top: 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.submit-btn.disabled {
background: #cccccc;
opacity: 0.6;
}
.loading-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
}
/* 实名认证弹窗样式 */
.auth-popup {
width: 600rpx;
max-width: 90vw;
background: white;
border-radius: 24rpx;
overflow: hidden;
}
.popup-header {
padding: 40rpx 40rpx 20rpx;
text-align: center;
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.popup-body {
padding: 40rpx;
text-align: center;
}
.auth-icon {
font-size: 80rpx;
display: block;
margin-bottom: 24rpx;
}
.auth-message {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 16rpx;
}
.auth-detail {
font-size: 26rpx;
color: #666;
display: block;
line-height: 1.5;
}
.popup-footer {
display: flex;
gap: 20rpx;
padding: 0 40rpx 40rpx;
}
.popup-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
border: none;
}
.popup-btn.secondary {
background: #f0f0f0;
color: #666;
}
.popup-btn.primary {
background: #007AFF;
color: white;
}
/* 错误弹窗样式 */
.error-popup {
width: 500rpx;
padding: 60rpx 40rpx;
background: white;
border-radius: 24rpx;
text-align: center;
}
.error-icon {
font-size: 60rpx;
display: block;
margin-bottom: 24rpx;
}
.error-message {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 40rpx;
line-height: 1.5;
}
.error-btn {
width: 100%;
height: 80rpx;
background: #007AFF;
color: white;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
/* 响应式设计 */
@media (max-width: 750px) {
.queue-container {
padding: 32rpx 24rpx;
}
.form-container {
padding: 40rpx 32rpx;
}
}
</style>

@ -0,0 +1,285 @@
<template>
<div class="page-container bg-white">
<div class="top-div">
<tn-tabs v-model="currentTabIndex" :bottom-shadow="false" :bar="false">
<TnTabsItem v-for="(item, index) in tabsData" :key="index" :title="item.text"
@click="changeStatus(item)" />
</tn-tabs>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<div class="tools-div">
<div style="width: 300px;">
<tn-input placeholder="请输入手机号搜索" height="32px" v-model="searchVal">
<template #prefix>
<uni-icons type="search" size="18" color="#ccc"></uni-icons>
</template>
</tn-input>
</div>
<div class="tools-btn">
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="shareToggle">
筛选<uni-icons type="down" size="12" color="#0099ff" style="margin-left: 5px;"></uni-icons>
</tn-button>
<div style="width: 2vw;"></div>
<tn-button width="80px" height="32px" text-color="#fff" @click="search">
<uni-icons type="search" size="18" color="#fff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
</div>
<div>
<uni-table ref="table" :loading="loading" border stripe emptyText="暂无更多数据">
<uni-tr>
<uni-th width="150" align="center">时间</uni-th>
<uni-th width="100" align="center">预约票号</uni-th>
<uni-th width="100" align="center">姓名</uni-th>
<uni-th width="120" align="center">手机号码</uni-th>
<uni-th width="100" align="center">业务类型</uni-th>
<uni-th width="180" align="center">预约时段</uni-th>
<uni-th width="180" align="center">状态</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in tableData" :key="index">
<uni-td align="center">{{ item.date }}</uni-td>
<uni-td align="center">{{ item.ticketNumber }}</uni-td>
<uni-td align="center">{{ item.customerName }}</uni-td>
<uni-td align="center">{{ item.phone }}</uni-td>
<uni-td align="center">{{ item.businessType }}</uni-td>
<uni-td align="center">{{ item.timeRange }}</uni-td>
<uni-td align="center">{{ item.status }}</uni-td>
</uni-tr>
</uni-table>
<div class="pagination-div">
<p><span class="page-span">{{ total }}</span>条数据每页显示<span class="page-span">{{ pageSize }}</span></p>
<view class="uni-pagination-box"><uni-pagination show-icon :page-size="pageSize" :current="pageCurrent"
:total="total" @change="change" /></view>
</div>
</div>
<uni-popup ref="share" type="share" safeArea backgroundColor="#fff">
<div class="popup-div">
<div class="fiter-items">
<TnCheckboxGroup v-model="selectValue">
<TnCheckbox v-for="item in bizData" :label="item.uid" size="lg" :border="true">{{ item.name }}
</TnCheckbox>
</TnCheckboxGroup>
</div>
<div class="filter-btn">
<tn-button width="80px" height="32px" :plain="true" text-color="#999" border-color="#999"
@click="resetSelectValue">
重置
</tn-button>
<div style="width: 2vw;"></div>
<tn-button width="80px" height="32px" text-color="#fff" @click="closeShareToggle">
确定
</tn-button>
</div>
</div>
</uni-popup>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import TnTabs from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs.vue'
import TnTabsItem from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs-item.vue'
import TnCheckbox from '@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox.vue'
import TnCheckboxGroup from '@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox-group.vue'
import { onLoad } from '@dcloudio/uni-app'
import { getAppointmentToday, getBizList } from '@/api/index.js'
onLoad(() => {
getData()
})
/*tabs变量*/
let currentTabIndex = ref(1)
let tabsData = [
{
text: '全部',
status: '-1'
},
{
text: '待换领',
status: '1'
},
{
text: '已换领',
status: '0'
},
{
text: '已过期',
status: '7'
},
{
text: '已作废',
status: '8'
}
]
/*表格变量*/
let searchVal = ref('')
const tableData = ref<any[]>([])
//
let pageSize = ref(10)
//
let pageCurrent = ref(1)
//
let total = ref(0)
let loading = ref(false)
//
let statusType = ref('1')
/*弹窗模块*/
const share = ref(null)
//
const bizData = ref<any[]>([])
//
const selectValue = ref<Number[]>([])
//
const change = (e) => {
// $refs.table.clearSelection()
// console.log(e)
pageCurrent.value = e.current
getData()
}
const getData = async () => {
const params = {
page: pageCurrent.value,
size: pageSize.value,
status: statusType.value,
bizUid: selectValue.value.join(','),
keyword: searchVal.value
}
console.log(params)
try {
const res = await getAppointmentToday(params)
loading.value = true
if (res && res.data) {
total.value = res.total || 0
pageCurrent.value = res.page || 1
//
tableData.value = res.data.map((item : any) => ({
date: item.date || '--',
ticketNumber: item.ticketNumber || '--',
customerName: item.customerName || '--',
phone: item.phone || '--',
businessType: item.businessType || '--',
timeRange: item.timeStart + '--' + item.timeEnd || '--',
status: item.status || 0
}))
} else {
tableData.value = []
total.value = 0
}
} catch (error) {
console.log('获取数据失败!', error)
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
tableData.value = []
} finally {
loading.value = false
}
}
const search = () => {
pageCurrent.value = 1
getData()
}
const shareToggle = async () => {
try {
const res = await getBizList()
console.log('业务列表:', res)
bizData.value = Array.isArray(res) ? res : []
share.value?.open()
} catch (error) {
console.error('获取业务列表失败:', error)
uni.showToast({
title: '获取筛选选项失败',
icon: 'none'
})
}
}
const resetSelectValue = () =>{
selectValue.value = []
}
const closeShareToggle = () => {
share.value?.close()
}
const changeStatus = (item) => {
console.log(item)
statusType.value = item.status
getData()
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss">
.bg-white {
background-color: #fff;
}
.top-div {
display: flex;
justify-content: space-between;
padding: 6vh 20px 1vh 20px;
}
.tools-div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
.tools-btn {
display: flex;
}
}
.pagination-div {
display: flex;
justify-content: space-between;
padding: 10px 20px;
.page-span {
display: inline;
color: #0099ff;
margin: 0 4px;
}
}
.uni-group {
display: flex;
align-items: center;
}
.popup-div {
.fiter-items {
padding: 50px 300px;
}
.filter-btn {
height: 6vh;
padding: 10px 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
</style>

@ -0,0 +1,277 @@
<template>
<div class="page-container bg-white">
<div class="top-div">
<tn-tabs v-model="currentTabIndex" :bottom-shadow="false" :bar="false">
<TnTabsItem v-for="(item, index) in tabsData" :key="index" :title="item.text"
@click="changeStatus(item)" />
</tn-tabs>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<div class="tools-div">
<div style="width: 300px;">
<tn-input placeholder="请输入票号搜索" height="32px" v-model="searchVal">
<template #prefix>
<uni-icons type="search" size="18" color="#ccc"></uni-icons>
</template>
</tn-input>
</div>
<div class="tools-btn">
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="shareToggle">
筛选<uni-icons type="down" size="12" color="#0099ff" style="margin-left: 5px;"></uni-icons>
</tn-button>
<div style="width: 2vw;"></div>
<tn-button width="80px" height="32px" text-color="#fff" @click="search">
<uni-icons type="search" size="18" color="#fff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
</div>
<div>
<uni-table ref="table" :loading="loading" border stripe emptyText="暂无更多数据">
<uni-tr>
<uni-th width="150" align="center">时间</uni-th>
<uni-th width="100" align="center">票号</uni-th>
<uni-th width="100" align="center">姓名</uni-th>
<uni-th width="220" align="center">身份证</uni-th>
<uni-th width="180" align="center">业务类型</uni-th>
<uni-th width="180" align="center">办理状态</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in tableData" :key="index">
<uni-td align="center">{{ item.time }}</uni-td>
<uni-td align="center">{{ item.ticketNumber }}</uni-td>
<uni-td align="center">{{ item.customerName }}</uni-td>
<uni-td align="center">{{ item.idCard }}</uni-td>
<uni-td align="center">{{ item.businessType }}</uni-td>
<uni-td align="center">{{ item.status }}</uni-td>
</uni-tr>
</uni-table>
<div class="pagination-div">
<p><span class="page-span">{{ total }}</span>条数据每页显示<span class="page-span">{{ pageSize }}</span></p>
<view class="uni-pagination-box"><uni-pagination show-icon :page-size="pageSize" :current="pageCurrent"
:total="total" @change="change" /></view>
</div>
</div>
<uni-popup ref="share" type="share" safeArea backgroundColor="#fff">
<div class="popup-div">
<div class="fiter-items">
<TnCheckboxGroup v-model="selectValue">
<TnCheckbox v-for="item in bizData" :label="item.uid" size="lg" :border="true">{{ item.name }}
</TnCheckbox>
</TnCheckboxGroup>
</div>
<div class="filter-btn">
<tn-button width="80px" height="32px" :plain="true" text-color="#999" border-color="#999"
@click="resetSelectValue">
重置
</tn-button>
<div style="width: 2vw;"></div>
<tn-button width="80px" height="32px" text-color="#fff" @click="closeShareToggle">
确定
</tn-button>
</div>
</div>
</uni-popup>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import TnTabs from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs.vue'
import TnTabsItem from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs-item.vue'
import TnCheckbox from '@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox.vue'
import TnCheckboxGroup from '@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox-group.vue'
import { onLoad } from '@dcloudio/uni-app'
import { getdailyEntry, getBizList } from '@/api/index.js'
onLoad(() => {
getData()
})
/*tabs变量*/
let currentTabIndex = ref(1)
let tabsData = [
{
text: '全部',
status: '-1'
},
{
text: '待办理',
status: '0'
},
{
text: '办理中',
status: '4'
},
{
text: '已办理',
status: '5,6'
}
]
/*表格变量*/
let searchVal = ref('')
const tableData = ref<any[]>([])
//
let pageSize = ref(10)
//
let pageCurrent = ref(1)
//
let total = ref(0)
let loading = ref(false)
//
let statusType = ref('0')
/*弹窗模块*/
const share = ref(null)
//
const bizData = ref<any[]>([])
//
const selectValue = ref<Number[]>([])
//
const change = (e) => {
// $refs.table.clearSelection()
// console.log(e)
pageCurrent.value = e.current
getData()
}
const getData = async () => {
const params = {
page: pageCurrent.value,
size: pageSize.value,
statusList: statusType.value,
bizUid: selectValue.value.join(','),
keyword: searchVal.value
}
console.log(params)
try {
const res = await getdailyEntry(params)
loading.value = true
if (res && res.data) {
total.value = res.total || 0
pageCurrent.value = res.page || 1
//
tableData.value = res.data.map((item : any) => ({
time: item.time || '--',
ticketNumber: item.ticketNumber || '--',
customerName: item.customerName || '--',
idCard: item.idCard || '--',
businessType: item.businessType || '--',
status: item.status || 0
}))
} else {
tableData.value = []
total.value = 0
}
} catch (error) {
console.log('获取数据失败!', error)
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
tableData.value = []
} finally {
loading.value = false
}
}
const search = () => {
pageCurrent.value = 1
getData()
}
const shareToggle = async () => {
try {
const res = await getBizList()
console.log('业务列表:', res)
bizData.value = Array.isArray(res) ? res : []
share.value?.open()
} catch (error) {
console.error('获取业务列表失败:', error)
uni.showToast({
title: '获取筛选选项失败',
icon: 'none'
})
}
}
const resetSelectValue = () =>{
selectValue.value = []
}
const closeShareToggle = () => {
share.value?.close()
}
const changeStatus = (item) => {
console.log(item)
statusType.value = item.status
getData()
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss">
.bg-white {
background-color: #fff;
}
.top-div {
display: flex;
justify-content: space-between;
padding: 6vh 20px 1vh 20px;
}
.tools-div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
.tools-btn {
display: flex;
}
}
.pagination-div {
display: flex;
justify-content: space-between;
padding: 10px 20px;
.page-span {
display: inline;
color: #0099ff;
margin: 0 4px;
}
}
.uni-group {
display: flex;
align-items: center;
}
.popup-div {
.fiter-items {
padding: 50px 300px;
}
.filter-btn {
height: 6vh;
padding: 10px 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
</style>

@ -0,0 +1,472 @@
<template>
<div class="page-container bg-white">
<div class="top-div">
<div class="tabs-div">
<!-- 一级菜单 -->
<div class="first-level-tabs">
<tn-tabs v-model="currentFirstLevelTab" :bottom-shadow="false" :bar="false" height="20px">
<TnTabsItem v-for="(item, index) in firstLevelTabs" :key="index" :title="item.text"
@click="changeFirstLevelTab(item)" />
</tn-tabs>
</div>
<!-- 二级菜单 -->
<div class="second-level-tabs">
<tn-tabs v-model="currentSecondLevelTab" :bottom-shadow="false" :bar="false" height="25px">
<TnTabsItem v-for="(item, index) in secondLevelTabs" :key="index" :title="item.text"
@click="changeSecondLevelTab(item)" />
</tn-tabs>
</div>
</div>
<tn-button width="80px" height="32px" :plain="true" text-color="#0099ff" @click="backToIndex">
<uni-icons type="arrow-left" size="18" color="#0099ff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
<div class="tools-div">
<div class="tool-input">
<tn-button width="11vw" height="32px" :plain="true" text-color="#0099ff"
@click="openDateTimePicker = !openDateTimePicker">
时间筛选<uni-icons type="down" size="12" color="#0099ff" style="margin-left: 5px;"></uni-icons>
</tn-button>
<div style="width: 1vw;"></div>
<tn-button width="11vw" height="32px" :plain="true" text-color="#0099ff" @click="shareToggle">
业务筛选<uni-icons type="down" size="12" color="#0099ff" style="margin-left: 5px;"></uni-icons>
</tn-button>
<div style="width: 1vw;"></div>
<tn-input placeholder="请输入票号搜索" height="36px" v-model="searchVal">
<template #prefix>
<uni-icons type="search" size="18" color="#ccc"></uni-icons>
</template>
</tn-input>
</div>
<div class="tools-btn">
<tn-button width="84px" height="32px" text-color="#fff" @click="search">
<uni-icons type="search" size="18" color="#fff" style="margin-right: 5px;"></uni-icons>
</tn-button>
</div>
</div>
<div>
<uni-table ref="table" :loading="loading" border stripe emptyText="暂无更多数据">
<uni-tr>
<uni-th width="180" align="center">公司名称</uni-th>
<uni-th width="100" align="center">姓名</uni-th>
<uni-th width="120" align="center">手机号码</uni-th>
<uni-th width="120" align="center">业务类型</uni-th>
<uni-th width="100" align="center">办理状态</uni-th>
<uni-th width="180" align="center">操作</uni-th>
</uni-tr>
<uni-tr v-for="(item, index) in tableData" :key="index">
<uni-td align="center">
<span class="click-able" @click="showReport = true">{{ item.companyName }}</span>
<TnIcon name="tip" size="24" color="#ff0000" />
</uni-td>
<uni-td align="center">{{ item.customerName+'('+item.roleType+')' }}</uni-td>
<uni-td align="center">{{ item.phoneNumber }}</uni-td>
<uni-td align="center">{{ item.bizName }}</uni-td>
<uni-td align="center">
<span class="click-able" @click="showDetail(item)">{{ item.tktId }}</span>
<span>{{ '('+item.status+')' }}</span>
</uni-td>
<uni-td>
<view class="uni-group">
<button v-show="!['5', '6'].includes(item.statusCode.toString())" class="uni-button"
size="mini" type="primary" @click="goToGuidance(item.tkt_id)">导税</button>
</view>
</uni-td>
</uni-tr>
</uni-table>
<div class="pagination-div">
<p><span class="page-span">{{ total }}</span>条数据每页显示<span class="page-span">{{ pageSize }}</span></p>
<view class="uni-pagination-box"><uni-pagination show-icon :page-size="pageSize" :current="pageCurrent"
:total="total" @change="change" /></view>
</div>
</div>
<!-- 税务健康检查报告组件 -->
<TaxHealthReport v-model:show="showReport" :report-data="customReportData" :company-info="customCompanyInfo"
title="自定义税务报告" @close="onClose" @confirm="onConfirm" />
<!-- 票号详情弹窗 -->
<QueueTicketDetail v-model:show="isShowDetail" :ticket-data="ticketDetail" title="排队票号详情"
@close="onDetailClose" />
<!-- 筛选弹窗 -->
<uni-popup ref="share" type="share" safeArea backgroundColor="#fff">
<div class="popup-div">
<div class="filter-btn">
<tn-button width="80px" height="32px" :plain="true" :text="true" text-color="#999"
border-color="#999" @click="resetSelectValue">
重置
</tn-button>
<tn-button width="80px" height="32px" :plain="true" :text="true" text-color="#0099ff"
@click="closeShareToggle">
确定
</tn-button>
</div>
<div class="fiter-items">
<TnCheckboxGroup v-model="selectValue">
<TnCheckbox v-for="item in bizData" :label="item.uid" size="lg" :border="true">{{ item.name }}
</TnCheckbox>
</TnCheckboxGroup>
</div>
</div>
</uni-popup>
<!-- 日期筛选弹窗 -->
<TnDateTimePicker v-model="dateTimeValue" v-model:open="openDateTimePicker" mode="timeNoSecond" />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import TnTabs from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs.vue'
import TnTabsItem from '@/uni_modules/tuniaoui-vue3/components/tabs/src/tabs-item.vue'
import TnCheckbox from '@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox.vue'
import TnCheckboxGroup from '@/uni_modules/tuniaoui-vue3/components/checkbox/src/checkbox-group.vue'
import TnBubbleBox from '@/uni_modules/tuniaoui-vue3/components/bubble-box/src/bubble-box.vue'
import TnIcon from '@/uni_modules/tuniaoui-vue3/components/icon/src/icon.vue'
import TnDateTimePicker from '@/uni_modules/tuniaoui-vue3/components/date-time-picker/src/date-time-picker.vue'
import { TaxHealthReport, defaultReportData } from '@/components/TaxHealthReport'
import type { ReportData } from '@/components/TaxHealthReport'
import { QueueTicketDetail, TicketStatus, RatingResult } from '@/components/QueueTicketDetail';
import type { TicketDetail } from '@/components/QueueTicketDetail';
import { onLoad } from '@dcloudio/uni-app'
import { getTicket, getBizList } from '@/api/index.js'
onLoad(() => {
getData()
})
/*一级菜单变量*/
let currentFirstLevelTab = ref(0)
let firstLevelTabs = [
{
text: '企业用户',
value: 'enterprise'
},
{
text: '个人用户',
value: 'personal'
}
]
/*二级菜单变量*/
let currentSecondLevelTab = ref(0)
let secondLevelTabs = computed(() => {
const baseTabs = [
{
text: '全部',
status: '-1'
},
{
text: '等候中',
status: '0'
},
{
text: '已完成',
status: '5,6'
}
]
//
if (firstLevelTabs[currentFirstLevelTab.value]?.value === 'enterprise') {
return [
...baseTabs,
{
text: '预约号',
status: 'appointment'
}
]
}
return baseTabs
})
/*表格变量*/
let searchVal = ref('')
const tableData = ref<any[]>([])
//
let pageSize = ref(10)
//
let pageCurrent = ref(1)
//
let total = ref(0)
let loading = ref(false)
//
let statusType = ref('0')
//
let userType = ref('enterprise')
/* 健康度报告模块 */
const showReport = ref(false);
/*弹窗模块*/
const share = ref(null)
//
const bizData = ref<any[]>([])
//
const selectValue = ref<Number[]>([])
/* 日期选择模块 */
const openDateTimePicker = ref(false)
const dateTimeValue = ref('')
//
const change = (e) => {
// $refs.table.clearSelection()
// console.log(e)
pageCurrent.value = e.current
getData()
}
//
const getData = async () => {
const params = {
page: pageCurrent.value,
size: pageSize.value,
status: statusType.value,
bizUid: selectValue.value.join(','),
keyword: searchVal.value,
userType: userType.value
}
console.log(params)
try {
const res = await getTicket(params)
loading.value = true
if (res && res.data) {
total.value = res.total || 0
pageCurrent.value = res.page || 1
//
tableData.value = res.data.map((item : any) => ({
//
customerName: item.customerName || '--',
companyName: item.companyName || '--',
taxNumber: item.taxNumber || '--',
phoneNumber: item.phoneNumber || '--',
roleType: item.roleType || '--',
//
tktId: item.tktId || '--',
tktDate: item.tktDate || '--',
tktTime: item.tktTime || '--',
window: item.window || -1,
winName: item.winName || '--',
bizUid: item.bizUid || -1,
bizName: item.bizName || '--',
businessType: item.businessType || '--',
id_code: item.id_code || '--',
status: item.status || '--',
statusCode: item.statusCode || 0
}))
} else {
tableData.value = []
total.value = 0
}
} catch (error) {
console.log('获取数据失败!', error)
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
tableData.value = []
} finally {
loading.value = false
}
}
// 使
const customReportData : ReportData = {
...defaultReportData,
registration: {
...defaultReportData.registration,
basicInfoConsistency: '自定义检查结果'
}
};
//
const onClose = () => {
console.log('弹窗关闭');
};
const onConfirm = () => {
console.log('确认操作');
};
const search = () => {
pageCurrent.value = 1
getData()
}
const shareToggle = async () => {
try {
const res = await getBizList()
console.log('业务列表:', res)
bizData.value = Array.isArray(res) ? res : []
share.value?.open()
} catch (error) {
console.error('获取业务列表失败:', error)
uni.showToast({
title: '获取筛选选项失败',
icon: 'none'
})
}
}
const resetSelectValue = () => {
selectValue.value = []
}
const closeShareToggle = () => {
share.value?.close()
}
/* 票号详情 */
const isShowDetail = ref(false)
//
const ticketDetail : TicketDetail = {
ticketNumber: 'B025',
status: TicketStatus.PROCESSING,
queueDate: '2024-01-15',
queueTime: '14:20:15',
serviceWindow: '5号窗口',
businessType: '发票领用',
staffName: '王工',
waitingDuration: '8分钟',
processingDuration: '进行中',
//
rating: RatingResult.EXCELLENT,
ratingPerson: '张先生',
ratingContact: '139****6666'
}
//
const showDetail = (item : any) => {
isShowDetail.value = !isShowDetail.value
ticketDetail.ticketNumber = item.tktId
ticketDetail.queueDate = item.tktDate
ticketDetail.serviceWindow = item.winName
ticketDetail.businessType = item.bizName
ticketDetail.ratingPerson = item.customerName
ticketDetail.ratingContact = item.phoneNumber
}
const onDetailClose = () => {
console.log('票号详情弹窗已关闭')
}
//
const changeFirstLevelTab = (item : any) => {
console.log('切换一级菜单:', item)
userType.value = item.value
//
currentSecondLevelTab.value = 0
statusType.value = secondLevelTabs.value[0]?.status || '0'
getData()
}
//
const changeSecondLevelTab = (item : any) => {
console.log('切换二级菜单:', item)
statusType.value = item.status
getData()
}
const goToGuidance = (item : any) => {
uni.navigateTo({
url: '/pages/mod/guidance?isFromTicket=true&tktId=' + item
})
}
const backToIndex = () => {
uni.navigateTo({
url: '/pages/index/index'
})
}
</script>
<style lang="scss">
.bg-white {
background-color: #fff;
}
.top-div {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 4vh 20px 1vh 20px;
flex-direction: row;
}
.tabs-div {
.first-level-tabs {
width: 100%;
}
.second-level-tabs {
width: 100%;
}
.tn-tabs-item__content {}
}
.tools-div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
.tool-input {
display: flex;
}
.tools-btn {
display: flex;
}
}
.click-able {
color: #0033ff;
text-decoration: underline;
}
.pagination-div {
display: flex;
justify-content: space-between;
padding: 10px 20px;
.page-span {
display: inline;
color: #0099ff;
margin: 0 4px;
}
}
.uni-group {
display: flex;
align-items: center;
}
.popup-div {
.fiter-items {
padding: 50px 300px;
}
.filter-btn {
height: 6vh;
padding: 10px 0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
</style>

@ -0,0 +1,82 @@
// utils/request.js
import {
ref
} from 'vue'
const baseURL = 'http://padapi.queuingsystem.cn/pad-api' // 替换为你的基础URL
// 全局加载状态,可根据需要在使用页面绑定
export const loading = ref(false)
const request = (options) => {
// 解构参数,并设置默认值
const {
url,
method = 'GET',
data = {},
withToken = true, // 默认携带Token
loading: showLoading = true, // 默认显示loading
loadingText = '加载中...'
} = options
return new Promise((resolve, reject) => {
// 请求开始显示loading
if (showLoading) {
loading.value = true
uni.showLoading({
title: loadingText,
mask: true
})
}
// 发起请求
uni.request({
url: baseURL + url,
method,
data,
header: {
'Content-Type': 'application/json',
// 根据配置决定是否携带Token
...(withToken && {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
})
},
success: (res) => {
console.log(res)
// 此处可根据后端返回结构调整
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 400) {
// HTTP状态码错误处理
uni.showToast({
title: `请求参数错误: ${res.statusCode}`,
icon: 'none'
})
reject(res)
} else if (res.statusCode === 401) {
// HTTP状态码错误处理
uni.showToast({
title: `用户名或密码错误: ${res.statusCode}`,
icon: 'none'
})
reject(res)
}
},
fail: (err) => {
uni.showToast({
title: '网络请求失败',
icon: 'none'
})
reject(err)
},
complete: () => {
// 隐藏loading
if (showLoading) {
loading.value = false
uni.hideLoading()
}
}
})
})
}
export default request

@ -0,0 +1,59 @@
// utils/validator.js
// 验证中文姓名2-10个汉字
export const validateChineseName = (name) => {
if (!name) {
return { valid: false, message: '姓名不能为空' };
}
const reg = /^[\u4e00-\u9fa5]{2,10}$/;
if (!reg.test(name)) {
return { valid: false, message: '请输入2-10个汉字' };
}
return { valid: true, message: '' };
};
// 验证手机号码
export const validatePhoneNumber = (phone) => {
if (!phone) {
return { valid: false, message: '手机号码不能为空' };
}
const reg = /^1[3-9]\d{9}$/;
if (!reg.test(phone)) {
return { valid: false, message: '请输入正确的手机号码' };
}
return { valid: true, message: '' };
};
// 验证身份证号码
export const validateIDCard = (idCard) => {
if (!idCard) {
return { valid: false, message: '身份证号码不能为空' };
}
// 基本格式验证
const reg = /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
if (!reg.test(idCard)) {
return { valid: false, message: '身份证格式不正确' };
}
// 校验码验证
const idCardArray = idCard.split('');
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(idCardArray[i]) * factor[i];
}
const mod = sum % 11;
if (parity[mod] !== idCardArray[17].toUpperCase()) {
return { valid: false, message: '身份证校验码错误' };
}
return { valid: true, message: '' };
};

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,3 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
shamefully-hoist=true

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

@ -0,0 +1,19 @@
import { app as e, BrowserWindow as r } from "electron";
import { fileURLToPath as t } from "url";
import o from "path";
const a = t(import.meta.url), i = o.dirname(a);
e.whenReady().then(() => {
const n = new r({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: !1,
contextIsolation: !0,
preload: o.join(i, "../preload/index.js")
}
});
process.env.VITE_DEV_SERVER_URL ? n.loadURL(process.env.VITE_DEV_SERVER_URL) : n.loadFile(o.join(i, "../../dist/index.html"));
});
e.on("window-all-closed", () => {
process.platform !== "darwin" && e.quit();
});

@ -0,0 +1,5 @@
import { contextBridge as o } from "electron";
o.exposeInMainWorld("electronAPI", {
// 可以在这里添加需要暴露的API
platform: process.platform
});

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ticket-client</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
{
"name": "ticket-client",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "dist-electron/main/index.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview",
"electron:dev": "vite dev --mode electron",
"electron:build": "vue-tsc -b && vite build --mode electron",
"electron:start": "electron ."
},
"dependencies": {
"vue": "^3.5.24"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"electron": "^40.0.0",
"electron-builder": "^26.4.0",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vite-plugin-electron": "^0.29.0",
"vue-tsc": "^3.1.4"
}
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,30 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

@ -0,0 +1,30 @@
import { app, BrowserWindow } from 'electron'
import { fileURLToPath } from 'url'
import path from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, '../preload/index.js')
}
})
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL as string)
} else {
win.loadFile(path.join(__dirname, '../../dist/index.html'))
}
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

@ -0,0 +1,8 @@
// 预加载脚本
import { contextBridge } from 'electron'
// 暴露API给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 可以在这里添加需要暴露的API
platform: process.platform
})

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), electron([
{
entry: 'src/electron/main/index.ts',
vite:{
build: {
outDir: 'dist-electron/main'
}
}
},
{
entry: 'src/electron/preload/index.ts',
vite:{
build: {
outDir: 'dist-electron/preload'
}
}
}
])],
})

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save