4 Commits

Author SHA1 Message Date
luo
f2344063ff fix: some 2026-10-14 11:18:29 +08:00
luo
37f24882c9 fix: 隐藏立即续费 2026-10-10 16:54:20 +08:00
luo
4d73421443 fix: 移除资产历史里面的激活时间 2026-09-23 14:31:24 +08:00
luo
b7b5a2b68c fix:后台地址兼容ip_address 2026-09-16 16:50:30 +08:00
4 changed files with 125 additions and 27 deletions

View File

@@ -25,8 +25,8 @@
{{ networkStatus == 1 ? '正常' : '停机' }}
</view>
</view>
<up-button v-if="hasPackageName" class="renew-button" type="primary" size="small"
@tap.stop="emit('renew')">立即续费</up-button>
<!-- <up-button v-if="hasPackageName" class="renew-button" type="primary" size="small"
@tap.stop="emit('renew')">立即续费</up-button> -->
</view>
</template>

View File

@@ -18,10 +18,6 @@
</view>
<view class="package-info">
<view class="info-row flex-row-sb">
<view class="info-label">激活时间</view>
<view class="info-value">{{ item.activated_at || '-' }}</view>
</view>
<view class="info-row flex-row-sb">
<view class="info-label">到期时间</view>
<view class="info-value">{{ item.expires_at || '-' }}</view>
@@ -137,13 +133,28 @@
loading.value = true;
try {
const data = await assetApi.getPackageHistory(
userStore.state.identifier,
page.value,
pageSize
);
let visibleItems;
do {
const requestedPage = page.value;
const data = await assetApi.getPackageHistory(
userStore.state.identifier,
requestedPage,
pageSize
);
const items = Array.isArray(data.items) ? data.items : [];
visibleItems = items.filter(item => Number(item?.status) !== 4);
const responseSize = Number(data.size) > 0 ? Number(data.size) : pageSize;
const responseTotal = data.total === null || data.total === undefined
? NaN : Number(data.total);
// Pagination describes the backend's unfiltered records, not the
// visible list. Skip fully hidden pages to reach subsequent records.
noMore.value = Number.isFinite(responseTotal) && responseTotal >= 0
? requestedPage * responseSize >= responseTotal
: items.length < pageSize;
page.value = requestedPage + 1;
} while (visibleItems.length === 0 && !noMore.value);
const newData = (data.items || []).map(item => ({
const newData = visibleItems.map(item => ({
...item,
activated_at: item.activated_at ? formatDate(item.activated_at) : '',
created_at: formatDate(item.created_at),
@@ -156,11 +167,6 @@
packageList.splice(0, packageList.length, ...newData);
}
if (newData.length < pageSize) {
noMore.value = true;
} else {
page.value++;
}
} catch (e) {
console.error('加载套餐历史失败', e);
}

View File

@@ -186,6 +186,7 @@
run_time: 0,
last_online_time: '-',
lan_ip: '-',
ip_address: '-',
kf_url: '-',
mchList: [],
imei: '-',
@@ -317,6 +318,7 @@
deviceInfo.ssidName = rt.ssid || '-';
deviceInfo.ssidPwd = rt.wifi_password || '-';
deviceInfo.lan_ip = rt.lan_ip || '-';
deviceInfo.ip_address = rt.ip_address || '-';
deviceInfo.connCnt = rt.client_number ?? 0;
deviceInfo.max_clients = rt.max_clients ?? 1;
deviceInfo.run_time = rt.run_time || 0;
@@ -329,6 +331,7 @@
deviceInfo.ssidName = '-';
deviceInfo.ssidPwd = '-';
deviceInfo.lan_ip = '-';
deviceInfo.ip_address = '-';
deviceInfo.connCnt = 0;
deviceInfo.max_clients = 1;
deviceInfo.run_time = 0;
@@ -452,7 +455,11 @@
};
const enterBack = async () => {
if (!deviceInfo.lan_ip || deviceInfo.lan_ip === '-') {
const lanIp = deviceInfo.lan_ip && deviceInfo.lan_ip !== '-' ? deviceInfo.lan_ip : '';
const remoteIp = deviceInfo.ip_address && deviceInfo.ip_address !== '-' ? deviceInfo.ip_address : '';
const backIp = lanIp || remoteIp;
if (!backIp) {
uni.showModal({
title: '提示',
content: '请将设备开机后, 等待2分钟左右进行连接WiFi',
@@ -462,18 +469,21 @@
return;
}
await new Promise((resolve) => {
uni.showModal({
title: '提示',
content: '请先连接当前设备WiFi,否则无法访问后台管理',
cancelText: '取消',
confirmText: '我知道了',
success: (res) => res.confirm && resolve()
// 局域网地址需先连接设备WiFi, 远程地址直接访问
if (lanIp) {
await new Promise((resolve) => {
uni.showModal({
title: '提示',
content: '请先连接当前设备WiFi,否则无法访问后台管理',
cancelText: '取消',
confirmText: '我知道了',
success: (res) => res.confirm && resolve()
});
});
});
}
// 跳转到设备后台管理地址
const url = deviceInfo.lan_ip.startsWith('http') ? deviceInfo.lan_ip : `http://${deviceInfo.lan_ip}`;
const url = backIp.startsWith('http') ? backIp : `http://${backIp}`;
// #ifdef H5
window.location.href = url;

View File

@@ -0,0 +1,82 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const vue = require('vue');
const source = readFileSync(new URL('../pages/asset-package-history/asset-package-history.vue', import.meta.url), 'utf8');
const plain = value => JSON.parse(JSON.stringify(value));
async function loadPage(responses) {
const requestedPages = [];
const context = vm.createContext({ console: { error() {} } });
const exports = {
vue: { ref: vue.ref, reactive: vue.reactive, onMounted() {} },
'@/api/index.js': { assetApi: { getPackageHistory: async (identifier, page, size) => {
assert.equal(identifier, 'A'); assert.equal(size, 10);
requestedPages.push(page);
return responses[page - 1] || { items: [] };
} } },
'@/store/index.js': { useUserStore: () => ({ state: { identifier: 'A' } }) }
};
const script = source.match(/<script setup>([\s\S]*?)<\/script>/)[1];
const page = new vm.SourceTextModule(script + '\nexport { loadPackageList, packageList, noMore, historyLoaded };', { context });
await page.link(specifier => {
const values = exports[specifier];
return new vm.SyntheticModule(Object.keys(values), function () {
for (const [key, value] of Object.entries(values)) this.setExport(key, value);
}, { context });
});
await page.evaluate();
return { page: page.namespace, requestedPages };
}
test('main package history hides status 4 and string 4, preserving other states and dates', async () => {
const { page } = await loadPage([{ items: [
{ status: 4 }, { status: '4' },
{ status: 0, package_name: '待生效', expires_at: '2026-10-14T12:00:00Z' },
{ status: 1 }, { status: 2 }, { status: 3 }
] }]);
await page.loadPackageList();
assert.deepEqual(plain(page.packageList).map(item => item.status), [0, 1, 2, 3]);
assert.equal(page.packageList[0].expires_at, '2026-10-14 12:00:00');
assert.equal(page.noMore.value, true);
});
test('filtering a full page must not make later records unreachable', async () => {
const { page, requestedPages } = await loadPage([
{ items: [...Array.from({ length: 9 }, () => ({ status: 4 })), { status: 1, id: 1 }] },
{ items: [{ status: 2, id: 2 }] }
]);
await page.loadPackageList();
assert.equal(page.noMore.value, false);
await page.loadPackageList(true);
assert.deepEqual(requestedPages, [1, 2]);
assert.deepEqual(plain(page.packageList).map(item => item.id), [1, 2]);
assert.equal(page.noMore.value, true);
});
test('fully invalidated first page is skipped automatically', async () => {
const { page, requestedPages } = await loadPage([
{ items: Array.from({ length: 10 }, () => ({ status: 4 })) },
{ items: [{ status: 1, id: 12 }] }
]);
await page.loadPackageList();
assert.deepEqual(requestedPages, [1, 2]);
assert.equal(page.packageList[0].id, 12);
assert.equal(page.noMore.value, true);
});
test('server total keeps pagination correct even when a filtered page is empty', async () => {
const { page, requestedPages } = await loadPage([
{ items: [{ status: '4' }], size: 10, total: 20 },
{ items: [{ status: 4 }], size: 10, total: 20 }
]);
await page.loadPackageList();
assert.deepEqual(requestedPages, [1, 2]);
assert.equal(page.packageList.length, 0);
assert.equal(page.noMore.value, true);
assert.equal(page.historyLoaded.value, true);
});