This commit is contained in:
che
2026-07-23 12:19:46 +05:00
parent dddf6516d3
commit 42029533fd
12 changed files with 603 additions and 11 deletions
+14 -1
View File
@@ -1,6 +1,6 @@
use che_tauri::{Filter, FilterSet}; use che_tauri::{Filter, FilterSet};
use super::models::{Contract, Counterparty}; use super::models::{Contract, ContractApplicationFile, Counterparty};
static CONTRACTAPP_FILTERS: &[Filter] = &[ static CONTRACTAPP_FILTERS: &[Filter] = &[
Filter::exact("id"), Filter::exact("id"),
@@ -30,6 +30,15 @@ static COUNTERPARTY_FILTERS: &[Filter] = &[
Filter::contains("name"), Filter::contains("name"),
]; ];
static CONTRACT_APPLICATION_FILE_FILTERS: &[Filter] = &[
Filter::exact("id"),
Filter::exact("contract_id"),
Filter::exact("name"),
Filter::contains("name"),
Filter::exact("file_type"),
Filter::exact("status"),
];
pub fn contractapp_filterset() -> FilterSet<Contract> { pub fn contractapp_filterset() -> FilterSet<Contract> {
FilterSet::new(CONTRACTAPP_FILTERS) FilterSet::new(CONTRACTAPP_FILTERS)
} }
@@ -37,3 +46,7 @@ pub fn contractapp_filterset() -> FilterSet<Contract> {
pub fn counterparty_filterset() -> FilterSet<Counterparty> { pub fn counterparty_filterset() -> FilterSet<Counterparty> {
FilterSet::new(COUNTERPARTY_FILTERS) FilterSet::new(COUNTERPARTY_FILTERS)
} }
pub fn contract_application_file_filterset() -> FilterSet<ContractApplicationFile> {
FilterSet::new(CONTRACT_APPLICATION_FILE_FILTERS)
}
@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS contract_application_file (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contract_id INTEGER NOT NULL REFERENCES contractapp(id),
name TEXT NOT NULL,
file_type TEXT NOT NULL,
file_type_display TEXT NOT NULL,
status TEXT NOT NULL,
status_display TEXT NOT NULL,
absolute_url TEXT NOT NULL,
scan_name TEXT NOT NULL,
scan_url TEXT NOT NULL,
local_path TEXT NOT NULL,
comment TEXT NOT NULL,
bill_total REAL NOT NULL,
bill_cost_total REAL NOT NULL,
questionnair INTEGER
);
@@ -1,5 +1,178 @@
{ {
"models": [ "models": [
{
"table": "contract_application_file",
"fields": [
{
"name": "id",
"ty": "integer",
"primary_key": true,
"nullable": false,
"auto": true,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "contract_id",
"ty": "integer",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": {
"table": "contractapp",
"column": "id"
}
},
{
"name": "name",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "file_type",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "file_type_display",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "status",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "status_display",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "absolute_url",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "scan_name",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "scan_url",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "local_path",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "comment",
"ty": "text",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "bill_total",
"ty": "real",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "bill_cost_total",
"ty": "real",
"primary_key": false,
"nullable": false,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
},
{
"name": "questionnair",
"ty": "integer",
"primary_key": false,
"nullable": true,
"auto": false,
"unique": false,
"max_length": null,
"default": null,
"foreign_key": null
}
]
},
{ {
"table": "contractapp", "table": "contractapp",
"fields": [ "fields": [
+5
View File
@@ -26,5 +26,10 @@ impl AppModule for ContractappModule {
serializers::counterparty_serializer(), serializers::counterparty_serializer(),
filters::counterparty_filterset(), filters::counterparty_filterset(),
); );
ctx.resource::<models::ContractApplicationFile>(
"contract_application_file",
serializers::contract_application_file_serializer(),
filters::contract_application_file_filterset(),
);
} }
} }
+24
View File
@@ -56,3 +56,27 @@ pub struct Counterparty {
pub name: String, pub name: String,
} }
#[derive(Debug, Clone, Model)]
#[model(table = "contract_application_file")]
pub struct ContractApplicationFile {
#[field(primary_key)]
pub id: i64,
#[field(foreign_key = Contract)]
pub contract_id: i64,
pub name: String,
pub file_type: String,
pub file_type_display: String,
pub status: String,
pub status_display: String,
pub absolute_url: String,
pub scan_name: String,
pub scan_url: String,
pub local_path: String,
pub comment: String,
pub bill_total: f64,
pub bill_cost_total: f64,
pub questionnair: Option<i64>,
}
+24 -1
View File
@@ -6,7 +6,7 @@ use crate::apps::{
projectapp::{models::Project, serializers::project_serializer}, projectapp::{models::Project, serializers::project_serializer},
}; };
use super::models::{Contract, Counterparty}; use super::models::{Contract, ContractApplicationFile, Counterparty};
static CONTRACT_FIELDS: &[Field] = &[ static CONTRACT_FIELDS: &[Field] = &[
Field::new("id").read_only(), Field::new("id").read_only(),
@@ -45,8 +45,27 @@ static CONTRACT_FIELDS: &[Field] = &[
]; ];
static COUNTERPARTY_FIELDS: &[Field] = &[Field::new("id").read_only(), Field::new("name")]; static COUNTERPARTY_FIELDS: &[Field] = &[Field::new("id").read_only(), Field::new("name")];
static CONTRACT_APPLICATION_FILE_FIELDS: &[Field] = &[
Field::new("id").read_only(),
Field::new("contract_id"),
Field::new("name"),
Field::new("file_type"),
Field::new("file_type_display"),
Field::new("status"),
Field::new("status_display"),
Field::new("absolute_url"),
Field::new("scan_name"),
Field::new("scan_url"),
Field::new("local_path"),
Field::new("comment"),
Field::new("bill_total"),
Field::new("bill_cost_total"),
Field::new("questionnair").required(false).nullable(),
Field::related("contract", "contract_id", &CONTRACT_RELATION),
];
static COUNTERPARTY_RELATION: RelatedModel<Counterparty> = static COUNTERPARTY_RELATION: RelatedModel<Counterparty> =
RelatedModel::new(counterparty_serializer); RelatedModel::new(counterparty_serializer);
static CONTRACT_RELATION: RelatedModel<Contract> = RelatedModel::new(contractapp_serializer);
static PROJECT_RELATION: RelatedModel<Project> = RelatedModel::new(project_serializer); static PROJECT_RELATION: RelatedModel<Project> = RelatedModel::new(project_serializer);
static FRC_RELATION: RelatedModel<Frc> = RelatedModel::new(frc_serializer); static FRC_RELATION: RelatedModel<Frc> = RelatedModel::new(frc_serializer);
static EMPLOYEE_RELATION: RelatedModel<Employee> = RelatedModel::new(employee_serializer); static EMPLOYEE_RELATION: RelatedModel<Employee> = RelatedModel::new(employee_serializer);
@@ -58,3 +77,7 @@ pub fn contractapp_serializer() -> ModelSerializer<Contract> {
pub fn counterparty_serializer() -> ModelSerializer<Counterparty> { pub fn counterparty_serializer() -> ModelSerializer<Counterparty> {
ModelSerializer::new(COUNTERPARTY_FIELDS) ModelSerializer::new(COUNTERPARTY_FIELDS)
} }
pub fn contract_application_file_serializer() -> ModelSerializer<ContractApplicationFile> {
ModelSerializer::new(CONTRACT_APPLICATION_FILE_FIELDS)
}
+10 -2
View File
@@ -2,6 +2,7 @@ pub mod apps;
pub mod sync; pub mod sync;
use che_tauri::{ApiError, ApiRequest, AppState, AuthTokenResponse, TauriApi}; use che_tauri::{ApiError, ApiRequest, AppState, AuthTokenResponse, TauriApi};
use tauri::Manager;
use crate::sync::SyncContractsResult; use crate::sync::SyncContractsResult;
@@ -43,8 +44,15 @@ fn auth_status(api: tauri::State<'_, TauriApi>) -> bool {
} }
#[tauri::command] #[tauri::command]
async fn sync_contracts(api: tauri::State<'_, TauriApi>) -> Result<SyncContractsResult, ApiError> { async fn sync_contracts(
sync::sync_contracts(&api).await app: tauri::AppHandle,
api: tauri::State<'_, TauriApi>,
) -> Result<SyncContractsResult, ApiError> {
let app_data_dir = app
.path()
.app_data_dir()
.map_err(|error| ApiError::new("file_error", error.to_string()))?;
sync::sync_contracts(&api, app_data_dir).await
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
+196 -2
View File
@@ -1,3 +1,5 @@
use std::path::PathBuf;
use che_orm::__private::sqlx; use che_orm::__private::sqlx;
use che_tauri::{ApiError, TauriApi}; use che_tauri::{ApiError, TauriApi};
use serde::Deserialize; use serde::Deserialize;
@@ -6,6 +8,8 @@ use serde::Deserialize;
pub struct SyncContractsResult { pub struct SyncContractsResult {
pub synced: usize, pub synced: usize,
pub pages: usize, pub pages: usize,
pub applications: usize,
pub files_downloaded: usize,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -66,6 +70,33 @@ struct RemoteBillCost {
paid_sum: Option<f64>, paid_sum: Option<f64>,
} }
#[derive(Debug, Deserialize)]
struct RemoteContractDetail {
#[serde(default)]
contractapplicationfile_set: Vec<RemoteContractApplicationFile>,
}
#[derive(Debug, Deserialize)]
struct RemoteContractApplicationFile {
id: i64,
name: String,
file_type: String,
#[serde(rename = "get_file_type_display")]
file_type_display: String,
status: String,
#[serde(rename = "get_status_display")]
status_display: String,
#[serde(rename = "get_absolute_url")]
absolute_url: String,
#[serde(rename = "get_scan")]
scan_name: String,
scan: String,
comment: String,
bill_total: f64,
bill_cost_total: f64,
questionnair: Option<i64>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct RemoteCompany { struct RemoteCompany {
id: i64, id: i64,
@@ -106,7 +137,10 @@ struct RemoteEmployee {
avatar_small: Option<String>, avatar_small: Option<String>,
} }
pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiError> { pub async fn sync_contracts(
api: &TauriApi,
app_data_dir: PathBuf,
) -> Result<SyncContractsResult, ApiError> {
let state = api.state(); let state = api.state();
let token = state let token = state
.auth_token() .auth_token()
@@ -123,6 +157,8 @@ pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiEr
)); ));
let mut synced = 0; let mut synced = 0;
let mut pages = 0; let mut pages = 0;
let mut applications = 0;
let mut files_downloaded = 0;
while let Some(url) = next_url { while let Some(url) = next_url {
let response = client let response = client
@@ -146,13 +182,116 @@ pub async fn sync_contracts(api: &TauriApi) -> Result<SyncContractsResult, ApiEr
for contract in &page.results { for contract in &page.results {
upsert_contract_dependencies(api, contract).await?; upsert_contract_dependencies(api, contract).await?;
upsert_contract(api, contract).await?; upsert_contract(api, contract).await?;
let detail = fetch_contract_detail(&client, &token, &remote.base_url, contract.id).await?;
for application in &detail.contractapplicationfile_set {
let local_path =
download_application_file(&client, &token, &app_data_dir, application).await?;
upsert_contract_application_file(api, contract.id, application, &local_path).await?;
applications += 1;
if !local_path.is_empty() {
files_downloaded += 1;
}
}
synced += 1; synced += 1;
} }
next_url = page.links.next; next_url = page.links.next;
} }
Ok(SyncContractsResult { synced, pages }) Ok(SyncContractsResult {
synced,
pages,
applications,
files_downloaded,
})
}
async fn fetch_contract_detail(
client: &reqwest::Client,
token: &str,
base_url: &str,
contract_id: i64,
) -> Result<RemoteContractDetail, ApiError> {
let response = client
.get(format!(
"{}/api/contract/{contract_id}/",
base_url.trim_end_matches('/')
))
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let detail = response.text().await.unwrap_or_default();
return Err(ApiError::new(
"remote_error",
format!("remote detail request failed with {status}: {detail}"),
));
}
Ok(response.json::<RemoteContractDetail>().await?)
}
async fn download_application_file(
client: &reqwest::Client,
token: &str,
app_data_dir: &PathBuf,
application: &RemoteContractApplicationFile,
) -> Result<String, ApiError> {
if application.scan.is_empty() {
return Ok(String::new());
}
let directory = application_download_dir(app_data_dir);
tokio::fs::create_dir_all(&directory)
.await
.map_err(file_error)?;
let file_name = format!("{}_{}", application.id, sanitize_file_name(&application.scan_name));
let path = directory.join(file_name);
let response = client
.get(&application.scan)
.header(reqwest::header::AUTHORIZATION, format!("Token {token}"))
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let detail = response.text().await.unwrap_or_default();
return Err(ApiError::new(
"remote_error",
format!("file download failed with {status}: {detail}"),
));
}
let bytes = response.bytes().await?;
tokio::fs::write(&path, bytes).await.map_err(file_error)?;
Ok(path.to_string_lossy().into_owned())
}
fn application_download_dir(app_data_dir: &PathBuf) -> PathBuf {
app_data_dir.join("contract_applications")
}
fn sanitize_file_name(file_name: &str) -> String {
let sanitized = file_name
.chars()
.map(|character| match character {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
_ => character,
})
.collect::<String>();
if sanitized.is_empty() {
"application_file".to_string()
} else {
sanitized
}
} }
async fn upsert_contract_dependencies( async fn upsert_contract_dependencies(
@@ -316,6 +455,61 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<()
Ok(()) Ok(())
} }
async fn upsert_contract_application_file(
api: &TauriApi,
contract_id: i64,
application: &RemoteContractApplicationFile,
local_path: &str,
) -> Result<(), ApiError> {
sqlx::query(
"INSERT INTO contract_application_file (
id, contract_id, name, file_type, file_type_display, status, status_display,
absolute_url, scan_name, scan_url, local_path, comment, bill_total,
bill_cost_total, questionnair
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
ON CONFLICT(id) DO UPDATE SET
contract_id = excluded.contract_id,
name = excluded.name,
file_type = excluded.file_type,
file_type_display = excluded.file_type_display,
status = excluded.status,
status_display = excluded.status_display,
absolute_url = excluded.absolute_url,
scan_name = excluded.scan_name,
scan_url = excluded.scan_url,
local_path = excluded.local_path,
comment = excluded.comment,
bill_total = excluded.bill_total,
bill_cost_total = excluded.bill_cost_total,
questionnair = excluded.questionnair",
)
.bind(application.id)
.bind(contract_id)
.bind(&application.name)
.bind(&application.file_type)
.bind(&application.file_type_display)
.bind(&application.status)
.bind(&application.status_display)
.bind(&application.absolute_url)
.bind(&application.scan_name)
.bind(&application.scan)
.bind(local_path)
.bind(&application.comment)
.bind(application.bill_total)
.bind(application.bill_cost_total)
.bind(application.questionnair)
.execute(api.state().db().pool())
.await
.map_err(database_error)?;
Ok(())
}
fn database_error(error: sqlx::Error) -> ApiError { fn database_error(error: sqlx::Error) -> ApiError {
ApiError::new("database_error", error.to_string()) ApiError::new("database_error", error.to_string())
} }
fn file_error(error: impl std::fmt::Display) -> ApiError {
ApiError::new("file_error", error.to_string())
}
+5
View File
@@ -4,6 +4,10 @@ import type {
ContractCreate, ContractCreate,
ContractUpdate, ContractUpdate,
ContractListParams, ContractListParams,
ContractApplicationFile,
ContractApplicationFileCreate,
ContractApplicationFileUpdate,
ContractApplicationFileListParams,
Counterparty, Counterparty,
CounterpartyCreate, CounterpartyCreate,
CounterpartyUpdate, CounterpartyUpdate,
@@ -35,6 +39,7 @@ import type {
} from "./models"; } from "./models";
export const contractApi = createModelApi<Contract, ContractCreate, ContractUpdate, ContractListParams>("contract"); export const contractApi = createModelApi<Contract, ContractCreate, ContractUpdate, ContractListParams>("contract");
export const contractApplicationFileApi = createModelApi<ContractApplicationFile, ContractApplicationFileCreate, ContractApplicationFileUpdate, ContractApplicationFileListParams>("contract_application_file");
export const counterpartyApi = createModelApi<Counterparty, CounterpartyCreate, CounterpartyUpdate, CounterpartyListParams>("counterparty"); export const counterpartyApi = createModelApi<Counterparty, CounterpartyCreate, CounterpartyUpdate, CounterpartyListParams>("counterparty");
export const employeeApi = createModelApi<Employee, EmployeeCreate, EmployeeUpdate, EmployeeListParams>("employee"); export const employeeApi = createModelApi<Employee, EmployeeCreate, EmployeeUpdate, EmployeeListParams>("employee");
export const frcApi = createModelApi<Frc, FrcCreate, FrcUpdate, FrcListParams>("frc"); export const frcApi = createModelApi<Frc, FrcCreate, FrcUpdate, FrcListParams>("frc");
+62
View File
@@ -120,6 +120,68 @@ export interface ContractListParams extends ListParams {
employee_id?: number | null; employee_id?: number | null;
} }
export interface ContractApplicationFile {
id: number;
contract_id: number;
name: string;
file_type: string;
file_type_display: string;
status: string;
status_display: string;
absolute_url: string;
scan_name: string;
scan_url: string;
local_path: string;
comment: string;
bill_total: number;
bill_cost_total: number;
questionnair: number | null;
contract: Contract;
}
export interface ContractApplicationFileCreate {
contract_id: number;
name: string;
file_type: string;
file_type_display: string;
status: string;
status_display: string;
absolute_url: string;
scan_name: string;
scan_url: string;
local_path: string;
comment: string;
bill_total: number;
bill_cost_total: number;
questionnair?: number | null;
}
export interface ContractApplicationFileUpdate {
contract_id?: number;
name?: string;
file_type?: string;
file_type_display?: string;
status?: string;
status_display?: string;
absolute_url?: string;
scan_name?: string;
scan_url?: string;
local_path?: string;
comment?: string;
bill_total?: number;
bill_cost_total?: number;
questionnair?: number | null;
}
export interface ContractApplicationFileListParams extends ListParams {
id?: number;
contract_id?: number;
name?: string;
name__contains?: string;
file_type?: string;
status?: string;
}
export interface Counterparty { export interface Counterparty {
id: number; id: number;
name: string; name: string;
+68 -4
View File
@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { openPath } from "@tauri-apps/plugin-opener";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { contractApi } from "../generated/api"; import { showToast } from "vant";
import type { Contract } from "../generated/models"; import { contractApi, contractApplicationFileApi } from "../generated/api";
import type { Contract, ContractApplicationFileListParams } from "../generated/models";
import { useModelApi } from "../composables/useModelApi"; import { useModelApi } from "../composables/useModelApi";
const route = useRoute(); const route = useRoute();
@@ -19,6 +21,19 @@ const {
autoLoad: false, autoLoad: false,
autoLoadOnFilterChange: false, autoLoadOnFilterChange: false,
}); });
const {
items: applicationFiles,
loading: loadingApplicationFiles,
error: applicationFilesError,
load: loadApplicationFiles,
} = useModelApi(contractApplicationFileApi, {
defaultListParams: { ordering: "id" } as ContractApplicationFileListParams,
loadErrorMessage: "Не удалось загрузить приложения",
autoLoad: false,
autoLoadOnFilterChange: false,
});
const viewError = computed(() => error.value || applicationFilesError.value);
type DetailField = { type DetailField = {
title: string; title: string;
@@ -87,23 +102,48 @@ function formatValue(value: unknown) {
return String(value); return String(value);
} }
async function openApplicationFile(localPath: string) {
if (!localPath) {
showToast("Файл не скачан");
return;
}
try {
await openPath(localPath);
} catch (err) {
showToast(errorMessage(err, "Не удалось открыть файл"));
}
}
function errorMessage(err: unknown, fallback: string) {
if (err instanceof Error) {
return err.message;
}
return fallback;
}
onMounted(() => { onMounted(() => {
if (Number.isFinite(contractId.value)) { if (Number.isFinite(contractId.value)) {
loadContract(contractId.value); loadContract(contractId.value);
loadApplicationFiles({
ordering: "id",
contract_id: contractId.value,
} as ContractApplicationFileListParams);
} }
}); });
</script> </script>
<template> <template>
<van-notice-bar <van-notice-bar
v-if="error" v-if="viewError"
class="notice" class="notice"
color="#991b1b" color="#991b1b"
background="#fee2e2" background="#fee2e2"
left-icon="warning-o" left-icon="warning-o"
wrapable wrapable
:scrollable="false" :scrollable="false"
:text="error" :text="viewError"
/> />
<section class="card detail-card"> <section class="card detail-card">
@@ -139,6 +179,30 @@ onMounted(() => {
/> />
</van-cell-group> </van-cell-group>
<van-cell-group inset title="Приложения">
<van-cell v-if="loadingApplicationFiles" title="Загрузка приложений..." />
<van-cell v-else-if="applicationFiles.length === 0" title="Приложений нет" />
<van-cell
v-for="file in applicationFiles"
v-else
:key="file.id"
:title="file.name"
:label="`${file.file_type_display} · ${file.status_display} · ${file.scan_name}`"
>
<template #value>
<van-button
size="small"
type="primary"
plain
:disabled="!file.local_path"
@click="openApplicationFile(file.local_path)"
>
Открыть
</van-button>
</template>
</van-cell>
</van-cell-group>
<div class="detail-actions"> <div class="detail-actions">
<van-button block round type="primary" plain @click="router.back()">Назад</van-button> <van-button block round type="primary" plain @click="router.back()">Назад</van-button>
</div> </div>
+5 -1
View File
@@ -59,6 +59,8 @@ const currentPage = computed({
interface SyncContractsResult { interface SyncContractsResult {
synced: number; synced: number;
pages: number; pages: number;
applications: number;
files_downloaded: number;
} }
const syncing = ref(false); const syncing = ref(false);
@@ -68,7 +70,9 @@ async function syncContracts() {
try { try {
const result = await invoke<SyncContractsResult>("sync_contracts"); const result = await invoke<SyncContractsResult>("sync_contracts");
showToast(`Синхронизировано: ${result.synced}`); showToast(
`Синхронизировано: ${result.synced}; приложений: ${result.applications}; файлов: ${result.files_downloaded}`,
);
if (filters.offset) { if (filters.offset) {
filters.offset = 0; filters.offset = 0;
} else { } else {