Răsfoiți Sursa

回滚代码

jinshihui 5 zile în urmă
părinte
comite
2cdfb4757f

+ 1 - 1
src/api/index.js

@@ -611,7 +611,7 @@ export function getServiceCategoryList(dictType) {
 export function getTechnicianInfo(data) {
     return request({
         method: 'get',
-        url:'/technician/technician/getTechnicianInfo',
+        url:'/technician/technician/profile',
         data
     });
 }

+ 1 - 1
src/common/config.js

@@ -12,7 +12,7 @@ let h5Url = 'https://city.baoxianzhanggui.com/fragrance'
 
 if (process.env.NODE_ENV == 'development') {
     // baseUrl = 'https://test.baoxianzhanggui.com/locallive-java' // 测试环境
-    // baseUrl = 'http://192.168.1.190:8087' // 靳
+     baseUrl = 'http://192.168.1.190:8087' // 靳
     // baseUrl = 'http://192.168.1.222:8087' // 志珺
 }
 

+ 4 - 3
src/pages/index/index.vue

@@ -311,6 +311,7 @@ import {
 	switchToOffline,
 	getSkillList
 } from '@/api/workbench.js';
+import { normalizeStorageValue } from '@/utils/technicianProfile.js';
 
 export default {
 	data() {
@@ -432,8 +433,8 @@ export default {
 			 * 服务标签(1:按摩推拿 2:陪玩)
 			 */
 			this.serviceTag = merchantInfo.merchant.serviceTag
-			uni.setStorageSync('serviceTag', merchantInfo.merchant.serviceTag)
-			uni.setStorageSync('userId', merchantInfo.merchant.id)
+			uni.setStorageSync('serviceTag', normalizeStorageValue(merchantInfo.merchant.serviceTag))
+			uni.setStorageSync('userId', normalizeStorageValue(merchantInfo.merchant.id))
 			return merchantInfo.merchant.auditStatus//用户状态
 
 		},
@@ -1812,4 +1813,4 @@ $page-bg: #f6f7f9;
 	text-overflow: ellipsis;
 	white-space: nowrap;
 }
-</style>
+</style>

+ 2 - 1
src/pages/login/wxLogin.vue

@@ -52,6 +52,7 @@
 <!-- <script href="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> -->
 <script ref="http://res.wx.qq.com/open/js/jweixin-1.6.0.js">
   import {phoneLogin} from "@/api/index";
+  import { normalizeStorageValue } from '@/utils/technicianProfile.js';
 	export default {
 		data() {
 			return {
@@ -234,7 +235,7 @@
               uni.setStorageSync('access-token', res.data.data.token)
 			  uni.setStorageSync('phone', res.data.data.cphone)
               uni.setStorageSync('wx_phone', res.data.data.cphone)
-              uni.setStorageSync('userId', res.data.data.id)
+              uni.setStorageSync('userId', normalizeStorageValue(res.data.data.id))
               _this.isBinding = false;
               _this.back();
             }

+ 4 - 3
src/pages/my/dynamic/index.vue

@@ -45,6 +45,7 @@
 
 <script>
 import { myDynamicList, draftDynamicList } from '@/api/dynamic.js'
+import { normalizeStorageValue } from '@/utils/technicianProfile.js'
 import MyDynamicList from './components/my_list.vue'
 import DraftDynamicList from './components/draft_list.vue'
 
@@ -90,8 +91,8 @@ export default {
        * 服务标签(1:按摩推拿 2:陪玩)
        */
       this.serviceTag = merchantInfo.merchant.serviceTag
-      uni.setStorageSync('serviceTag', merchantInfo.merchant.serviceTag)
-      uni.setStorageSync('userId', merchantInfo.merchant.id)
+      uni.setStorageSync('serviceTag', normalizeStorageValue(merchantInfo.merchant.serviceTag))
+      uni.setStorageSync('userId', normalizeStorageValue(merchantInfo.merchant.id))
       return merchantInfo.merchant.auditStatus//用户状态
 
     },
@@ -253,4 +254,4 @@ export default {
     }
   }
 }
-</style>
+</style>

+ 16 - 2
src/utils/index.js

@@ -1,6 +1,7 @@
 import statusManager from '@/utils/statusManager.js';
 import addressService from '@/utils/address.js';
 import {getTechnicianInfo} from '@/api/index';
+import { adaptTechnicianProfile, getOpenidFromStorage } from '@/utils/technicianProfile.js';
 
 // 日期格式化原型扩展(单独维护)
 Date.prototype.Format = function (fmt) {
@@ -202,9 +203,11 @@ const otherUtil = {
 	//查询商户状态
 	async checkMerchantStatus(openid) {
 		try {
-			const response = await getTechnicianInfo({ openid });
+			const profileOpenid = getStoredProfileOpenid() || openid;
+			if (!profileOpenid) return null;
+			const response = await getTechnicianInfo({ openid: profileOpenid });
 			if (response.data.code === 200) {
-			return response.data.data;
+				return adaptTechnicianProfile(response.data.data);
 			}
 			// 接口非200返回null
 			return null;
@@ -220,6 +223,17 @@ const otherUtil = {
 	addressService,
 };
 
+function getStoredProfileOpenid() {
+	// #ifdef H5
+	if (typeof window !== 'undefined') {
+		const openid = getOpenidFromStorage(window.localStorage);
+		if (openid) return openid;
+	}
+	// #endif
+
+	return uni.getStorageSync('wx_copenid') || '';
+}
+
 // 组合导出
 export default {
 	...formatUtil,

+ 54 - 0
src/utils/technicianProfile.js

@@ -0,0 +1,54 @@
+export function adaptTechnicianProfile(profile = {}) {
+	const merchant = {
+		...profile,
+		id: profile.merchantId,
+		merchantId: profile.merchantId,
+		teName: profile.name || '',
+		teSex: profile.sex,
+		tePhone: profile.phone || '',
+		teNickName: profile.nickName?.value || '',
+		teNickNamePendingValue: profile.nickName?.pendingValue || '',
+		teNickNameAuditStatus: profile.nickName?.auditStatus ?? null,
+		teNickNameAuditStatusText: profile.nickName?.auditStatusText || '',
+		teNickNameAuditRemark: profile.nickName?.auditRemark || '',
+		teBrief: profile.brief?.value || '',
+		teBriefPendingValue: profile.brief?.pendingValue || '',
+		teBriefAuditStatus: profile.brief?.auditStatus ?? null,
+		teBriefAuditStatusText: profile.brief?.auditStatusText || '',
+		teBriefAuditRemark: profile.brief?.auditRemark || '',
+		avatar: profile.avatar || '',
+		cPortrait: profile.avatar || '',
+		auditStatus: profile.auditStatus,
+		serviceTag: profile.serviceTag,
+	}
+
+	return {
+		merchant,
+		merchantAuditFile: getOfficialFiles(profile.fileGroups),
+		fileGroups: Array.isArray(profile.fileGroups) ? profile.fileGroups : [],
+	}
+}
+
+export function getOfficialFiles(fileGroups) {
+	if (!Array.isArray(fileGroups)) return []
+
+	return fileGroups.reduce((list, group) => {
+		const officialFiles = Array.isArray(group.officialFiles) ? group.officialFiles : []
+		const files = officialFiles.map(file => ({
+			...file,
+			fileType: group.fileType,
+			fileTypeName: group.fileTypeName,
+		}))
+		return list.concat(files)
+	}, [])
+}
+
+export function getOpenidFromStorage(storage) {
+	if (!storage || typeof storage.getItem !== 'function') return ''
+	return storage.getItem('wx_copenid') || ''
+}
+
+export function normalizeStorageValue(value) {
+	if (value === null || value === undefined) return ''
+	return String(value)
+}

+ 150 - 0
src/utils/technicianProfile.test.js

@@ -0,0 +1,150 @@
+import { adaptTechnicianProfile, getOpenidFromStorage, normalizeStorageValue } from './technicianProfile'
+
+describe('technicianProfile', () => {
+	test('adapts profile response data to legacy merchant shape', () => {
+		const result = adaptTechnicianProfile({
+			merchantId: 12,
+			name: '靳世辉',
+			sex: 1,
+			phone: '19834500372',
+			address: null,
+			areaCode: null,
+			avatar: '',
+			provinceCode: '110000',
+			provinceName: '北京市',
+			cityCode: '110000',
+			cityName: '北京市',
+			operationCenterId: 8801,
+			operationCenterName: '华北区运营中心',
+			auditStatus: 2,
+			serviceTag: '1',
+			nickName: {
+				value: '小靳',
+				pendingValue: '测试',
+				auditStatus: 0,
+				auditStatusText: '审核中',
+				auditRemark: null,
+				editable: false,
+			},
+			brief: {
+				value: '这是我的简介',
+				pendingValue: null,
+				auditStatus: null,
+				auditStatusText: null,
+				auditRemark: null,
+				editable: true,
+			},
+			fileGroups: [
+				{
+					fileType: '1',
+					fileTypeName: '形象照',
+					officialFiles: [
+						{
+							id: 302,
+							fileName: '形象照',
+							fileUrl: 'https://example.com/a.jpeg',
+							fileSize: 9.12,
+							contentType: 'image/jpeg',
+							applyBatchNo: null,
+							auditStatus: 1,
+							auditRemark: null,
+						},
+					],
+					pendingFiles: [
+						{
+							id: 319,
+							fileName: '待审形象照',
+							fileUrl: 'https://example.com/pending.jpeg',
+							fileSize: 54.45,
+							contentType: 'jpeg',
+							applyBatchNo: 'batch',
+							auditStatus: 0,
+							auditRemark: null,
+						},
+					],
+				},
+				{
+					fileType: '2',
+					fileTypeName: '生活照',
+					officialFiles: [
+						{
+							id: 308,
+							fileName: '生活照1',
+							fileUrl: 'https://example.com/life.png',
+							fileSize: 28.17,
+							contentType: 'image/png',
+							applyBatchNo: null,
+							auditStatus: 1,
+							auditRemark: null,
+						},
+					],
+					pendingFiles: [],
+				},
+			],
+		})
+
+		expect(result.merchant).toMatchObject({
+			id: 12,
+			merchantId: 12,
+			teName: '靳世辉',
+			teSex: 1,
+			tePhone: '19834500372',
+			teNickName: '小靳',
+			teBrief: '这是我的简介',
+			provinceCode: '110000',
+			provinceName: '北京市',
+			cityCode: '110000',
+			cityName: '北京市',
+			operationCenterId: 8801,
+			operationCenterName: '华北区运营中心',
+			auditStatus: 2,
+			serviceTag: '1',
+			teNickNameAuditStatus: 0,
+			teBriefAuditStatus: null,
+		})
+		expect(result.merchantAuditFile).toEqual([
+			{
+				id: 302,
+				fileName: '形象照',
+				fileUrl: 'https://example.com/a.jpeg',
+				fileSize: 9.12,
+				contentType: 'image/jpeg',
+				applyBatchNo: null,
+				auditStatus: 1,
+				auditRemark: null,
+				fileType: '1',
+				fileTypeName: '形象照',
+			},
+			{
+				id: 308,
+				fileName: '生活照1',
+				fileUrl: 'https://example.com/life.png',
+				fileSize: 28.17,
+				contentType: 'image/png',
+				applyBatchNo: null,
+				auditStatus: 1,
+				auditRemark: null,
+				fileType: '2',
+				fileTypeName: '生活照',
+			},
+		])
+	})
+
+	test('reads openid from wx_copenid storage key', () => {
+		const storage = {
+			getItem(key) {
+				return key === 'wx_copenid' ? 'openid-value' : ''
+			},
+		}
+
+		expect(getOpenidFromStorage(storage)).toBe('openid-value')
+	})
+
+	test('normalizes numeric storage values to plain strings', () => {
+		expect(normalizeStorageValue(12)).toBe('12')
+		expect(normalizeStorageValue(1)).toBe('1')
+		expect(normalizeStorageValue('2')).toBe('2')
+		expect(normalizeStorageValue(null)).toBe('')
+		expect(normalizeStorageValue(undefined)).toBe('')
+	})
+})

+ 122 - 0
src/workbench/contract/contractRecords.js

@@ -0,0 +1,122 @@
+export function formatSignTime(value) {
+	if (!value) return ''
+	const date = new Date(value)
+	if (Number.isNaN(date.getTime())) return String(value)
+
+	const pad = num => String(num).padStart(2, '0')
+	return [
+		date.getFullYear(),
+		pad(date.getMonth() + 1),
+		pad(date.getDate()),
+	].join('.') + ` ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+}
+
+export function getFileType(fileUrl = '') {
+	const path = String(fileUrl).split('?')[0].toLowerCase()
+	if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(path)) return 'image'
+	if (/\.pdf$/.test(path)) return 'pdf'
+	return 'unknown'
+}
+
+export function normalizeContractRecords(payload) {
+	if (!payload || !Array.isArray(payload.file)) return []
+
+	const signTime = formatSignTime(payload.signTime)
+	return payload.file.map(file => ({
+		id: file.id,
+		contractName: file.contractName || '合同文件',
+		signTime,
+		signerName: payload.signerName || '',
+		merchantId: payload.merchantId,
+		fileUrl: file.fileUrl || '',
+		fileType: getFileType(file.fileUrl),
+	}))
+}
+
+export function resolveContractFileUrl(fileUrl = '', baseUrl = '') {
+	if (!fileUrl) return ''
+	if (/^https?:\/\//i.test(fileUrl)) return fileUrl
+	if (!fileUrl.startsWith('/')) return fileUrl
+
+	try {
+		const { origin } = new URL(baseUrl)
+		return `${origin}${fileUrl}`
+	} catch (e) {
+		return fileUrl
+	}
+}
+
+export function getUserIdFromSessionStorage(storage) {
+	if (!storage || typeof storage.getItem !== 'function') return ''
+
+	const directUserId = storage.getItem('userId')
+	if (directUserId) return directUserId
+
+	const keys = ['userInfo', 'user', 'merchantInfo', 'technicianInfo']
+	for (const key of keys) {
+		const userId = getUserIdFromStorageValue(storage.getItem(key))
+		if (userId) return userId
+	}
+
+	if (typeof storage.key === 'function') {
+		for (let index = 0; index < storage.length; index += 1) {
+			const key = storage.key(index)
+			if (keys.includes(key) || key === 'userId') continue
+
+			const userId = getUserIdFromStorageValue(storage.getItem(key))
+			if (userId) return userId
+		}
+	}
+
+	return ''
+}
+
+function getUserIdFromStorageValue(raw) {
+	if (!raw) return ''
+	try {
+		return findUserId(JSON.parse(raw), 0, 'userInfo')
+	} catch (e) {
+		return ''
+	}
+}
+
+function findUserId(value, depth = 0, parentKey = '') {
+	if (!value || typeof value !== 'object' || depth > 5) return ''
+
+	const directUserId = value.userId || value.merchantId
+	if (directUserId) return directUserId
+
+	if (value.id && (isKnownUserKey(parentKey) || isLikelyUserObject(value))) return value.id
+
+	const preferredKeys = ['merchant', 'technician', 'user', 'userInfo', 'data', 'result']
+	for (const key of preferredKeys) {
+		const userId = findUserId(value[key], depth + 1, key)
+		if (userId) return userId
+	}
+
+	for (const key of Object.keys(value)) {
+		if (preferredKeys.includes(key)) continue
+		const userId = findUserId(value[key], depth + 1, key)
+		if (userId) return userId
+	}
+
+	return ''
+}
+
+function isKnownUserKey(key) {
+	return ['merchant', 'technician', 'user', 'userInfo', 'merchantInfo', 'technicianInfo'].includes(key)
+}
+
+function isLikelyUserObject(value) {
+	return Boolean(
+		value.userId ||
+		value.merchantId ||
+		value.auditStatus !== undefined ||
+		value.serviceTag !== undefined ||
+		value.cOpenid ||
+		value.copenid ||
+		value.cPhone ||
+		value.cphone ||
+		value.token
+	)
+}

+ 118 - 0
src/workbench/contract/contractRecords.test.js

@@ -0,0 +1,118 @@
+import {
+	formatSignTime,
+	getFileType,
+	normalizeContractRecords,
+	getUserIdFromSessionStorage,
+	resolveContractFileUrl,
+} from './contractRecords'
+
+describe('contractRecords', () => {
+	test('normalizes contract API payload into file cards', () => {
+		const records = normalizeContractRecords({
+			signTime: '2026-06-04T17:09:37.000+08:00',
+			signerName: '靳世辉',
+			merchantId: 12,
+			file: [
+				{
+					id: 1,
+					contractName: '合同图片.png',
+					fileUrl: '/profile/upload/contract.png',
+				},
+				{
+					id: 5,
+					contractName: '我的合同',
+					fileUrl: 'https://example.com/contract.pdf',
+				},
+			],
+		})
+
+		expect(records).toEqual([
+			{
+				id: 1,
+				contractName: '合同图片.png',
+				signTime: '2026.06.04 17:09:37',
+				signerName: '靳世辉',
+				merchantId: 12,
+				fileUrl: '/profile/upload/contract.png',
+				fileType: 'image',
+			},
+			{
+				id: 5,
+				contractName: '我的合同',
+				signTime: '2026.06.04 17:09:37',
+				signerName: '靳世辉',
+				merchantId: 12,
+				fileUrl: 'https://example.com/contract.pdf',
+				fileType: 'pdf',
+			},
+		])
+	})
+
+	test('returns an empty list for missing file arrays', () => {
+		expect(normalizeContractRecords(null)).toEqual([])
+		expect(normalizeContractRecords({ file: null })).toEqual([])
+	})
+
+	test('resolves relative upload urls against the API origin', () => {
+		expect(
+			resolveContractFileUrl('/profile/upload/a.png', 'https://city.baoxianzhanggui.com/nightFragrance')
+		).toBe('https://city.baoxianzhanggui.com/profile/upload/a.png')
+		expect(resolveContractFileUrl('https://example.com/a.png', 'https://host/api')).toBe(
+			'https://example.com/a.png'
+		)
+	})
+
+	test('formats date and detects supported file types', () => {
+		expect(formatSignTime('2026-06-04T17:09:37.000+08:00')).toBe('2026.06.04 17:09:37')
+		expect(formatSignTime('')).toBe('')
+		expect(getFileType('a.jpeg')).toBe('image')
+		expect(getFileType('a.PDF')).toBe('pdf')
+		expect(getFileType('http://example.com/file')).toBe('unknown')
+	})
+
+	test('reads userId from session storage user information', () => {
+		const storage = {
+			getItem(key) {
+				const values = {
+					userInfo: JSON.stringify({ id: 12 }),
+				}
+				return values[key] || ''
+			},
+		}
+
+		expect(getUserIdFromSessionStorage(storage)).toBe(12)
+	})
+
+	test('falls back to direct session storage userId', () => {
+		const storage = {
+			getItem(key) {
+				return key === 'userId' ? '12' : ''
+			},
+		}
+
+		expect(getUserIdFromSessionStorage(storage)).toBe('12')
+	})
+
+	test('scans unknown session storage keys for nested merchant ids', () => {
+		const values = {
+			currentUser: JSON.stringify({
+				data: {
+					merchant: {
+						id: 12,
+					},
+				},
+			}),
+		}
+		const storage = {
+			length: 1,
+			key(index) {
+				return Object.keys(values)[index]
+			},
+			getItem(key) {
+				return values[key] || ''
+			},
+		}
+
+		expect(getUserIdFromSessionStorage(storage)).toBe(12)
+	})
+})

+ 58 - 8
src/workbench/contract/detail.vue

@@ -1,14 +1,15 @@
 <template>
 	<view class="detail-page">
-		<!-- PDF 预览 -->
 		<web-view v-if="fileType === 'pdf' && fileUrl" :src="fileUrl"></web-view>
 
-		<!-- 图片预览 -->
 		<scroll-view v-else-if="fileType === 'image' && fileUrl" scroll-y class="image-scroll">
 			<image class="contract-image" :src="fileUrl" mode="widthFix" @click="previewImage" />
 		</scroll-view>
 
-		<view class="empty" v-else>暂无合同文件</view>
+		<view class="empty" v-else>
+			<text class="empty-text">{{ fileUrl ? '暂不支持预览该合同文件' : '暂无合同文件' }}</text>
+			<view class="open-btn" v-if="fileUrl" @click="openFile">打开文件</view>
+		</view>
 	</view>
 </template>
 
@@ -21,10 +22,12 @@ export default {
 		}
 	},
 	onLoad(query) {
-		this.fileType = query.fileType || 'pdf'
+		this.fileType = query.fileType || 'unknown'
 		this.fileUrl = decodeURIComponent(query.fileUrl || '')
 		if (query.title) {
-			uni.setNavigationBarTitle({ title: query.title })
+			uni.setNavigationBarTitle({
+				title: decodeURIComponent(query.title),
+			})
 		}
 	},
 	methods: {
@@ -35,6 +38,34 @@ export default {
 				current: this.fileUrl,
 			})
 		},
+		openFile() {
+			if (!this.fileUrl) return
+
+			// #ifdef H5
+			if (typeof window !== 'undefined') {
+				window.open(this.fileUrl, '_blank')
+				return
+			}
+			// #endif
+
+			uni.downloadFile({
+				url: this.fileUrl,
+				success: res => {
+					if (res.statusCode === 200) {
+						uni.openDocument({
+							filePath: res.tempFilePath,
+							showMenu: true,
+						})
+					}
+				},
+				fail: () => {
+					uni.showToast({
+						title: '文件打开失败',
+						icon: 'none',
+					})
+				},
+			})
+		},
 	},
 }
 </script>
@@ -55,9 +86,28 @@ export default {
 }
 
 .empty {
-	text-align: center;
-	padding: 120rpx 0;
+	min-height: 100vh;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding-top: 120rpx;
+	box-sizing: border-box;
+}
+
+.empty-text {
+	font-size: 28rpx;
+	color: #999999;
+	line-height: 40rpx;
+}
+
+.open-btn {
+	margin-top: 32rpx;
+	height: 72rpx;
+	line-height: 72rpx;
+	padding: 0 40rpx;
+	border-radius: 36rpx;
+	background: #1d2129;
+	color: #ffffff;
 	font-size: 28rpx;
-	color: #999;
 }
 </style>

+ 102 - 33
src/workbench/contract/index.vue

@@ -7,48 +7,115 @@
 				:key="item.id"
 				@click="viewContract(item)"
 			>
-				<text class="contract-name">{{ item.title }}</text>
-				<text class="contract-meta">签定时间:{{ item.signTime }}</text>
-				<text class="contract-meta">签订人:{{ item.signatory }}</text>
+				<text class="contract-name">{{ item.contractName }}</text>
+				<text class="contract-meta">签定时间:{{ item.signTime || '-' }}</text>
+				<text class="contract-meta">签订人:{{ item.signerName || '-' }}</text>
 			</view>
 		</view>
-		<view class="empty" v-else>暂无合同</view>
+
+		<view class="empty" v-else-if="!loading">
+			{{ errorText || '暂无合同' }}
+		</view>
 	</view>
 </template>
 
 <script>
-import { MOCK_CONTRACT_LIST } from './mock.js'
+import { baseUrl } from '@/common/config.js'
 import { getContractRecords } from '@/api/workbench.js'
+import {
+	getUserIdFromSessionStorage,
+	normalizeContractRecords,
+	resolveContractFileUrl,
+} from './contractRecords.js'
 
 export default {
 	data() {
 		return {
 			contractList: [],
+			loading: false,
+			errorText: '',
 		}
 	},
 	onShow() {
 		this.loadList()
 	},
 	methods: {
+		getBrowserStorageUserId() {
+			// #ifdef H5
+			if (typeof window !== 'undefined') {
+				return (
+					getUserIdFromSessionStorage(window.sessionStorage) ||
+					getUserIdFromSessionStorage(window.localStorage)
+				)
+			}
+			// #endif
+			return ''
+		},
+		getUserId() {
+			return this.getBrowserStorageUserId() || uni.getStorageSync('userId')
+		},
 		loadList() {
-			// 接口就绪后替换
-			getContractRecords({
-				userId: uni.getStorageSync('userId'),
-			}).then(res => {
-				console.log(res)
-				if (res.data.code == 200) {
-					this.contractList = res.data.data
-				}
-			})
+			const userId = this.getUserId()
+			if (!userId) {
+				this.contractList = []
+				this.errorText = '未获取到用户信息'
+				uni.showToast({
+					title: this.errorText,
+					icon: 'none',
+				})
+				return
+			}
+
+			this.loading = true
+			this.errorText = ''
+			getContractRecords({ userId })
+				.then(res => {
+					const body = res && res.data ? res.data : {}
+					if (body.code === 200) {
+						this.contractList = normalizeContractRecords(body.data)
+						this.errorText = ''
+						return
+					}
+
+					this.contractList = []
+					this.errorText = body.msg || '合同获取失败'
+					uni.showToast({
+						title: this.errorText,
+						icon: 'none',
+					})
+				})
+				.catch(() => {
+					this.contractList = []
+					this.errorText = '合同获取失败'
+					uni.showToast({
+						title: this.errorText,
+						icon: 'none',
+					})
+				})
+				.finally(() => {
+					this.loading = false
+				})
 		},
 		viewContract(item) {
-			const str = uni.$u.queryParams({
-				id: item.id,
-				title: item.title,
-				fileType: item.fileType,
-				fileUrl: item.fileUrl,
+			const fileUrl = resolveContractFileUrl(item.fileUrl, baseUrl)
+			if (!fileUrl) {
+				uni.showToast({
+					title: '暂无合同文件',
+					icon: 'none',
+				})
+				return
+			}
+
+			const query = [
+				`id=${encodeURIComponent(item.id)}`,
+				`title=${encodeURIComponent(item.contractName)}`,
+				`fileType=${encodeURIComponent(item.fileType)}`,
+				`fileUrl=${encodeURIComponent(fileUrl)}`,
+			].join('&')
+
+			uni.navigateTo({
+				url: `/workbench/contract/detail?${query}`,
 			})
-			uni.navigateTo({ url: `/workbench/contract/detail${str}` })
 		},
 	},
 }
@@ -58,41 +125,43 @@ export default {
 .contract-page {
 	min-height: 100vh;
 	background: #f5f5f5;
-	padding: 24rpx;
+	padding: 16rpx;
 	box-sizing: border-box;
 }
 
 .contract-list {
 	display: flex;
 	flex-direction: column;
-	gap: 20rpx;
 }
 
 .contract-card {
-	background: #fff;
-	border-radius: 12rpx;
-	padding: 28rpx 32rpx;
+	background: #ffffff;
+	border-radius: 8rpx;
+	padding: 24rpx;
+	margin-bottom: 16rpx;
+	box-sizing: border-box;
 }
 
 .contract-name {
 	display: block;
-	font-size: 30rpx;
-	color: #333;
+	font-size: 28rpx;
+	color: #1d2129;
 	font-weight: 600;
-	margin-bottom: 16rpx;
+	line-height: 40rpx;
+	margin-bottom: 18rpx;
 }
 
 .contract-meta {
 	display: block;
-	font-size: 26rpx;
-	color: #999;
-	line-height: 1.6;
+	font-size: 24rpx;
+	color: #4e5969;
+	line-height: 38rpx;
 }
 
 .empty {
 	text-align: center;
-	padding: 120rpx 0;
+	padding-top: 72rpx;
 	font-size: 28rpx;
-	color: #999;
+	color: #999999;
 }
 </style>